diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b421a869dd..ed04f95c04c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ ## vNEXT (not yet released) +## v3.18.1 + +### `@liveblocks/react-ui` + +- Mentions suggestions now appear in more cases after typing `@`: + - After punctuation like `!`, `.`, `(`, etc. (e.g. `Hello!@`, + `cc: the other team (@`) + - After emojis (e.g. `Hello 👋@`) + ## v3.18.0 For full upgrade instructions, see the diff --git a/e2e/next-sandbox/test/comments/composer.test.ts b/e2e/next-sandbox/test/comments/composer.test.ts index 98a81f39dba..5bc3ea649d2 100644 --- a/e2e/next-sandbox/test/comments/composer.test.ts +++ b/e2e/next-sandbox/test/comments/composer.test.ts @@ -520,6 +520,17 @@ test.describe("Composer", () => { ]); }); + test("should not support mentions preceded by non-whitespace characters", async () => { + const { editor } = getComposer(page); + + await editor.pressSequentially("Hello stacy@example"); + await sleep(100); + + await expect( + page.locator(".lb-composer-mention-suggestions-list") + ).not.toBeVisible(); + }); + test("should support non-alphanumeric characters in mentions but not leading or consecutive whitespace", async () => { const { editor } = getComposer(page); @@ -562,6 +573,35 @@ test.describe("Composer", () => { ).not.toBeVisible(); }); + test("should support mentions preceded by emojis", async () => { + const { editor } = getComposer(page); + + await editor.pressSequentially("Hello 👋@"); + + await expect( + page.locator(".lb-composer-suggestions-list-item").first() + ).toBeVisible(); + }); + + test("should support mentions preceded by some specific punctuations", async () => { + const { editor } = getComposer(page); + + await editor.pressSequentially("Hello!@"); + await expect( + page.locator(".lb-composer-suggestions-list-item").first() + ).toBeVisible(); + + await editor.pressSequentially("Hello.@"); + await expect( + page.locator(".lb-composer-suggestions-list-item").first() + ).toBeVisible(); + + await editor.pressSequentially("Hello (@"); + await expect( + page.locator(".lb-composer-suggestions-list-item").first() + ).toBeVisible(); + }); + test("should support user and group mentions", async () => { const { editor } = getComposer(page); diff --git a/examples/nextjs-3d-builder/.env.example b/examples/nextjs-3d-builder/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-3d-builder/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-3d-builder/package-lock.json b/examples/nextjs-3d-builder/package-lock.json index e85031303cf..77ca15ab627 100644 --- a/examples/nextjs-3d-builder/package-lock.json +++ b/examples/nextjs-3d-builder/package-lock.json @@ -560,15 +560,15 @@ "license": "Apache-2.0" }, "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -582,9 +582,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -598,9 +598,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -614,9 +614,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -630,9 +630,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -646,9 +646,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -662,9 +662,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -678,9 +678,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -1309,12 +1309,12 @@ } }, "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.1", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -1328,14 +1328,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/examples/nextjs-ai-app-builder/.env.example b/examples/nextjs-ai-app-builder/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-app-builder/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-ai-calendar/.env.example b/examples/nextjs-ai-calendar/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-calendar/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-ai-chats/.env.example b/examples/nextjs-ai-chats/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-chats/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-ai-dashboard-reports/.env.example b/examples/nextjs-ai-dashboard-reports/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-dashboard-reports/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-ai-popup/.env.example b/examples/nextjs-ai-popup/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-popup/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-ai-support/.env.example b/examples/nextjs-ai-support/.env.example new file mode 100644 index 00000000000..3a7ff90fad7 --- /dev/null +++ b/examples/nextjs-ai-support/.env.example @@ -0,0 +1,5 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/copilots +NEXT_PUBLIC_LIVEBLOCKS_COPILOT_ID= diff --git a/examples/nextjs-blocknote/.env.example b/examples/nextjs-blocknote/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-blocknote/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-ag-grid/.env.example b/examples/nextjs-comments-ag-grid/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-ag-grid/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-audio/.env.example b/examples/nextjs-comments-audio/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-audio/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-canvas/.env.example b/examples/nextjs-comments-canvas/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-canvas/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-canvas/package-lock.json b/examples/nextjs-comments-canvas/package-lock.json index 48561d53e08..20e16b73e5d 100644 --- a/examples/nextjs-comments-canvas/package-lock.json +++ b/examples/nextjs-comments-canvas/package-lock.json @@ -1986,15 +1986,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.1.tgz", - "integrity": "sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.1.tgz", - "integrity": "sha512-BwZ8w8YTaSEr2HIuXLMLxIdElNMPvY9fLqb20LX9A9OMGtJilhHLbCL3ggyd0TwjmMcTxi0XXt+ur1vWUoxj2Q==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -2008,9 +2008,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -2024,9 +2024,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -2040,9 +2040,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -2056,9 +2056,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -2072,9 +2072,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -2088,9 +2088,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -2104,9 +2104,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -2637,12 +2637,12 @@ } }, "node_modules/next": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.1.tgz", - "integrity": "sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.1", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -2656,14 +2656,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/examples/nextjs-comments-emails-resend/.env.example b/examples/nextjs-comments-emails-resend/.env.example new file mode 100644 index 00000000000..5fa3be98669 --- /dev/null +++ b/examples/nextjs-comments-emails-resend/.env.example @@ -0,0 +1,8 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/webhooks +LIVEBLOCKS_WEBHOOK_SECRET_KEY= + +# https://resend.com/api-keys +RESEND_API_KEY= diff --git a/examples/nextjs-comments-emails-sendgrid/.env.example b/examples/nextjs-comments-emails-sendgrid/.env.example new file mode 100644 index 00000000000..099dd9346d8 --- /dev/null +++ b/examples/nextjs-comments-emails-sendgrid/.env.example @@ -0,0 +1,8 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/webhooks +LIVEBLOCKS_WEBHOOK_SECRET_KEY= + +# https://app.sendgrid.com/settings/api_keys +SENDGRID_API_KEY= diff --git a/examples/nextjs-comments-emails-sendgrid/package-lock.json b/examples/nextjs-comments-emails-sendgrid/package-lock.json index 08153bce3ec..4258bee9831 100644 --- a/examples/nextjs-comments-emails-sendgrid/package-lock.json +++ b/examples/nextjs-comments-emails-sendgrid/package-lock.json @@ -1962,15 +1962,15 @@ } }, "node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -1984,9 +1984,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -2000,9 +2000,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -2016,9 +2016,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -2032,9 +2032,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -2048,9 +2048,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -2064,9 +2064,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -2080,9 +2080,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -2481,12 +2481,15 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.18", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", - "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/call-bind-apply-helpers": { @@ -2939,14 +2942,14 @@ } }, "node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -2958,15 +2961,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/examples/nextjs-comments-notifications/.env.example b/examples/nextjs-comments-notifications/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-notifications/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-overlay/.env.example b/examples/nextjs-comments-overlay/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-overlay/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-primitives/.env.example b/examples/nextjs-comments-primitives/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-primitives/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-search/.env.example b/examples/nextjs-comments-search/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-search/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-tiptap/.env.example b/examples/nextjs-comments-tiptap/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-tiptap/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-video/.env.example b/examples/nextjs-comments-video/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-video/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments/.env.example b/examples/nextjs-comments/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-connection-status/.env.example b/examples/nextjs-connection-status/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-connection-status/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-dashboard/.env.example b/examples/nextjs-dashboard/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-dashboard/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-form/.env.example b/examples/nextjs-form/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-form/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-lexical-emails-resend/.env.example b/examples/nextjs-lexical-emails-resend/.env.example new file mode 100644 index 00000000000..5fa3be98669 --- /dev/null +++ b/examples/nextjs-lexical-emails-resend/.env.example @@ -0,0 +1,8 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/webhooks +LIVEBLOCKS_WEBHOOK_SECRET_KEY= + +# https://resend.com/api-keys +RESEND_API_KEY= diff --git a/examples/nextjs-lexical/.env.example b/examples/nextjs-lexical/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-lexical/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-live-avatars-advanced/.env.example b/examples/nextjs-live-avatars-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-live-avatars-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-live-avatars/.env.example b/examples/nextjs-live-avatars/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-live-avatars/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-live-cursors-advanced/.env.example b/examples/nextjs-live-cursors-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-live-cursors-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-live-cursors-chat/.env.example b/examples/nextjs-live-cursors-chat/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-live-cursors-chat/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-live-cursors-scroll/.env.example b/examples/nextjs-live-cursors-scroll/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-live-cursors-scroll/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-live-cursors-scroll/package-lock.json b/examples/nextjs-live-cursors-scroll/package-lock.json index 04116428865..b6dc8d612ea 100644 --- a/examples/nextjs-live-cursors-scroll/package-lock.json +++ b/examples/nextjs-live-cursors-scroll/package-lock.json @@ -594,15 +594,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.0.tgz", - "integrity": "sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.0.tgz", - "integrity": "sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -616,9 +616,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.0.tgz", - "integrity": "sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -632,9 +632,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.0.tgz", - "integrity": "sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -648,9 +648,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.0.tgz", - "integrity": "sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -664,9 +664,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.0.tgz", - "integrity": "sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -680,9 +680,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.0.tgz", - "integrity": "sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -696,9 +696,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.0.tgz", - "integrity": "sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.0.tgz", - "integrity": "sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -1371,12 +1371,12 @@ } }, "node_modules/next": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz", - "integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.0", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -1390,14 +1390,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.0", - "@next/swc-darwin-x64": "16.2.0", - "@next/swc-linux-arm64-gnu": "16.2.0", - "@next/swc-linux-arm64-musl": "16.2.0", - "@next/swc-linux-x64-gnu": "16.2.0", - "@next/swc-linux-x64-musl": "16.2.0", - "@next/swc-win32-arm64-msvc": "16.2.0", - "@next/swc-win32-x64-msvc": "16.2.0", + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/examples/nextjs-live-cursors/.env.example b/examples/nextjs-live-cursors/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-live-cursors/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-live-form-selection/.env.example b/examples/nextjs-live-form-selection/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-live-form-selection/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-nextauth-google-avatars/.env.example b/examples/nextjs-nextauth-google-avatars/.env.example new file mode 100644 index 00000000000..5736d51b8ff --- /dev/null +++ b/examples/nextjs-nextauth-google-avatars/.env.example @@ -0,0 +1,9 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://console.cloud.google.com/apis/credentials +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +# Generate with `openssl rand -base64 32` +JWT_SECRET= diff --git a/examples/nextjs-notification-settings/.env.example b/examples/nextjs-notification-settings/.env.example new file mode 100644 index 00000000000..4ec8dfa3c9b --- /dev/null +++ b/examples/nextjs-notification-settings/.env.example @@ -0,0 +1,11 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/webhooks +LIVEBLOCKS_WEBHOOK_SECRET_KEY= + +# https://resend.com/api-keys +RESEND_API_KEY= + +# Generate with `openssl rand -base64 32` +NEXTAUTH_SECRET= diff --git a/examples/nextjs-notifications-custom/.env.example b/examples/nextjs-notifications-custom/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-notifications-custom/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-react-flow/package-lock.json b/examples/nextjs-react-flow/package-lock.json index 8326f408748..218d3c13bd8 100644 --- a/examples/nextjs-react-flow/package-lock.json +++ b/examples/nextjs-react-flow/package-lock.json @@ -690,11 +690,15 @@ } }, "node_modules/@next/env": { - "version": "16.2.1", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.2.1", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -708,12 +712,13 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.1.tgz", - "integrity": "sha512-/vrcE6iQSJq3uL3VGVHiXeaKbn8Es10DGTGRJnRZlkNQQk3kaNtAJg8Y6xuAlrx/6INKVjkfi5rY0iEXorZ6uA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -723,12 +728,13 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.1.tgz", - "integrity": "sha512-uLn+0BK+C31LTVbQ/QU+UaVrV0rRSJQ8RfniQAHPghDdgE+SlroYqcmFnO5iNjNfVWCyKZHYrs3Nl0mUzWxbBw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -738,12 +744,13 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.1.tgz", - "integrity": "sha512-ssKq6iMRnHdnycGp9hCuGnXJZ0YPr4/wNwrfE5DbmvEcgl9+yv97/Kq3TPVDfYome1SW5geciLB9aiEqKXQjlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -753,12 +760,13 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.1.tgz", - "integrity": "sha512-HQm7SrHRELJ30T1TSmT706IWovFFSRGxfgUkyWJZF/RKBMdbdRWJuFrcpDdE5vy9UXjFOx6L3mRdqH04Mmx0hg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -768,12 +776,13 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.1.tgz", - "integrity": "sha512-aV2iUaC/5HGEpbBkE+4B8aHIudoOy5DYekAKOMSHoIYQ66y/wIVeaRx8MS2ZMdxe/HIXlMho4ubdZs/J8441Tg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -783,12 +792,13 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.1.tgz", - "integrity": "sha512-IXdNgiDHaSk0ZUJ+xp0OQTdTgnpx1RCfRTalhn3cjOP+IddTMINwA7DXZrwTmGDO8SUr5q2hdP/du4DcrB1GxA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -798,12 +808,13 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.1.tgz", - "integrity": "sha512-qvU+3a39Hay+ieIztkGSbF7+mccbbg1Tk25hc4JDylf8IHjYmY/Zm64Qq1602yPyQqvie+vf5T/uPwNxDNIoeg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -3332,10 +3343,12 @@ } }, "node_modules/next": { - "version": "16.2.1", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.2.1", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -3349,14 +3362,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.2.1", - "@next/swc-darwin-x64": "16.2.1", - "@next/swc-linux-arm64-gnu": "16.2.1", - "@next/swc-linux-arm64-musl": "16.2.1", - "@next/swc-linux-x64-gnu": "16.2.1", - "@next/swc-linux-x64-musl": "16.2.1", - "@next/swc-win32-arm64-msvc": "16.2.1", - "@next/swc-win32-x64-msvc": "16.2.1", + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", "sharp": "^0.34.5" }, "peerDependencies": { diff --git a/examples/nextjs-spreadsheet-advanced/.env.example b/examples/nextjs-spreadsheet-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-spreadsheet-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-tiptap-advanced/.env.example b/examples/nextjs-tiptap-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-tiptap-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-tiptap-ai/.env.example b/examples/nextjs-tiptap-ai/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-tiptap-ai/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-tiptap-emails-resend/.env.example b/examples/nextjs-tiptap-emails-resend/.env.example new file mode 100644 index 00000000000..5fa3be98669 --- /dev/null +++ b/examples/nextjs-tiptap-emails-resend/.env.example @@ -0,0 +1,8 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= + +# https://liveblocks.io/dashboard/webhooks +LIVEBLOCKS_WEBHOOK_SECRET_KEY= + +# https://resend.com/api-keys +RESEND_API_KEY= diff --git a/examples/nextjs-tiptap/.env.example b/examples/nextjs-tiptap/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-tiptap/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-todo-list/.env.example b/examples/nextjs-todo-list/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-todo-list/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-whiteboard-advanced/.env.example b/examples/nextjs-whiteboard-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-whiteboard-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-whiteboard/.env.example b/examples/nextjs-whiteboard/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/nextjs-whiteboard/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/nextjs-yjs-blocknote-advanced/.env.example b/examples/nextjs-yjs-blocknote-advanced/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-blocknote-advanced/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-blocknote/.env.example b/examples/nextjs-yjs-blocknote/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-blocknote/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-codemirror/.env.example b/examples/nextjs-yjs-codemirror/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-codemirror/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-lexical/.env.example b/examples/nextjs-yjs-lexical/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-lexical/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-monaco/.env.example b/examples/nextjs-yjs-monaco/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-monaco/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-quill/.env.example b/examples/nextjs-yjs-quill/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-quill/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-slate/.env.example b/examples/nextjs-yjs-slate/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-slate/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-yjs-slate/package-lock.json b/examples/nextjs-yjs-slate/package-lock.json index baaae5feef1..29c1b63a741 100644 --- a/examples/nextjs-yjs-slate/package-lock.json +++ b/examples/nextjs-yjs-slate/package-lock.json @@ -604,15 +604,15 @@ } }, "node_modules/@next/env": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", - "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.3.tgz", + "integrity": "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", - "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.3.tgz", + "integrity": "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==", "cpu": [ "arm64" ], @@ -626,9 +626,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", - "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.3.tgz", + "integrity": "sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==", "cpu": [ "x64" ], @@ -642,9 +642,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", - "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.3.tgz", + "integrity": "sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==", "cpu": [ "arm64" ], @@ -658,9 +658,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", - "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.3.tgz", + "integrity": "sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==", "cpu": [ "arm64" ], @@ -674,9 +674,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", - "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.3.tgz", + "integrity": "sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==", "cpu": [ "x64" ], @@ -690,9 +690,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", - "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.3.tgz", + "integrity": "sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==", "cpu": [ "x64" ], @@ -706,9 +706,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", - "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.3.tgz", + "integrity": "sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==", "cpu": [ "arm64" ], @@ -722,9 +722,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", - "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.3.tgz", + "integrity": "sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==", "cpu": [ "x64" ], @@ -1031,12 +1031,12 @@ } }, "node_modules/next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", - "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.3.tgz", + "integrity": "sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==", "license": "MIT", "dependencies": { - "@next/env": "16.1.7", + "@next/env": "16.2.3", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -1050,15 +1050,15 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.7", - "@next/swc-darwin-x64": "16.1.7", - "@next/swc-linux-arm64-gnu": "16.1.7", - "@next/swc-linux-arm64-musl": "16.1.7", - "@next/swc-linux-x64-gnu": "16.1.7", - "@next/swc-linux-x64-musl": "16.1.7", - "@next/swc-win32-arm64-msvc": "16.1.7", - "@next/swc-win32-x64-msvc": "16.1.7", - "sharp": "^0.34.4" + "@next/swc-darwin-arm64": "16.2.3", + "@next/swc-darwin-x64": "16.2.3", + "@next/swc-linux-arm64-gnu": "16.2.3", + "@next/swc-linux-arm64-musl": "16.2.3", + "@next/swc-linux-x64-gnu": "16.2.3", + "@next/swc-linux-x64-musl": "16.2.3", + "@next/swc-win32-arm64-msvc": "16.2.3", + "@next/swc-win32-x64-msvc": "16.2.3", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", diff --git a/examples/nextjs-yjs-tiptap/.env.example b/examples/nextjs-yjs-tiptap/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-yjs-tiptap/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nuxtjs-live-avatars/.env.example b/examples/nuxtjs-live-avatars/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nuxtjs-live-avatars/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/react-comments/.env.example b/examples/react-comments/.env.example new file mode 100644 index 00000000000..54bda2a4c41 --- /dev/null +++ b/examples/react-comments/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/redux-todo-list/.env.example b/examples/redux-todo-list/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/redux-todo-list/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/redux-whiteboard/.env.example b/examples/redux-whiteboard/.env.example new file mode 100644 index 00000000000..50a10803af5 --- /dev/null +++ b/examples/redux-whiteboard/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/sveltekit-live-avatars/.env.example b/examples/sveltekit-live-avatars/.env.example new file mode 100644 index 00000000000..4a4267df750 --- /dev/null +++ b/examples/sveltekit-live-avatars/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_SECRET_KEY= diff --git a/examples/sveltekit-live-cursors/.env.example b/examples/sveltekit-live-cursors/.env.example new file mode 100644 index 00000000000..4a4267df750 --- /dev/null +++ b/examples/sveltekit-live-cursors/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_SECRET_KEY= diff --git a/examples/vuejs-live-avatars/.env.example b/examples/vuejs-live-avatars/.env.example new file mode 100644 index 00000000000..54bda2a4c41 --- /dev/null +++ b/examples/vuejs-live-avatars/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/vuejs-live-cursors/.env.example b/examples/vuejs-live-cursors/.env.example new file mode 100644 index 00000000000..54bda2a4c41 --- /dev/null +++ b/examples/vuejs-live-cursors/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/zustand-todo-list/.env.example b/examples/zustand-todo-list/.env.example new file mode 100644 index 00000000000..54bda2a4c41 --- /dev/null +++ b/examples/zustand-todo-list/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_PUBLIC_KEY= diff --git a/examples/zustand-whiteboard/.env.example b/examples/zustand-whiteboard/.env.example new file mode 100644 index 00000000000..54bda2a4c41 --- /dev/null +++ b/examples/zustand-whiteboard/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +VITE_LIVEBLOCKS_PUBLIC_KEY= diff --git a/package-lock.json b/package-lock.json index a1b1f362f0b..e22c7f70354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "prettier": "^3.4.2", "publint": "^0.3.6", "ts-morph": "^22.0.0", - "tsd": "^0.31.2", "tsup": "^8.3.5", "turbo": "^2.5.6", "typescript": "^5.7.2", @@ -17323,14 +17322,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@tsd/typescript": { - "version": "5.4.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.17" - } - }, "node_modules/@types/aria-query": { "version": "5.0.1", "dev": true, @@ -17611,15 +17602,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "7.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.6", "license": "MIT" @@ -22167,42 +22149,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-formatter-pretty": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "^7.2.13", - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "eslint-rule-docs": "^1.1.5", - "log-symbols": "^4.0.0", - "plur": "^4.0.0", - "string-width": "^4.2.0", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-formatter-pretty/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "license": "MIT", @@ -22496,11 +22442,6 @@ "eslint": ">=5.0.0" } }, - "node_modules/eslint-rule-docs": { - "version": "1.1.235", - "dev": true, - "license": "MIT" - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -24697,14 +24638,6 @@ "node": ">= 0.10" } }, - "node_modules/irregular-plurals": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-accessor-descriptor": { "version": "1.0.1", "license": "MIT", @@ -27570,50 +27503,6 @@ "version": "5.2.1", "license": "MIT" }, - "node_modules/meow": { - "version": "9.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "20.2.9", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -29461,20 +29350,6 @@ "node": ">=18" } }, - "node_modules/plur": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "irregular-plurals": "^3.2.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/posix-character-classes": { "version": "0.1.1", "license": "MIT", @@ -33586,18 +33461,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "license": "MIT", @@ -34361,28 +34224,6 @@ "node": ">=4" } }, - "node_modules/tsd": { - "version": "0.31.2", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.31.2.tgz", - "integrity": "sha512-VplBAQwvYrHzVihtzXiUVXu5bGcr7uH1juQZ1lmKgkuGNGT+FechUCqmx9/zk7wibcqR2xaNEwCkDyKh+VVZnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tsd/typescript": "~5.4.3", - "eslint-formatter-pretty": "^4.1.0", - "globby": "^11.0.1", - "jest-diff": "^29.0.3", - "meow": "^9.0.0", - "path-exists": "^4.0.0", - "read-pkg-up": "^7.0.0" - }, - "bin": { - "tsd": "dist/cli.js" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -37537,11 +37378,11 @@ }, "packages/liveblocks-chat-sdk-adapter": { "name": "@liveblocks/chat-sdk-adapter", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0" + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37554,10 +37395,10 @@ }, "packages/liveblocks-client": { "name": "@liveblocks/client", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0" + "@liveblocks/core": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37566,7 +37407,7 @@ }, "packages/liveblocks-core": { "name": "@liveblocks/core", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37584,11 +37425,11 @@ }, "packages/liveblocks-emails": { "name": "@liveblocks/emails", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0" + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37607,10 +37448,10 @@ }, "packages/liveblocks-node": { "name": "@liveblocks/node", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0", + "@liveblocks/core": "3.18.1", "@stablelib/base64": "^1.0.1", "fast-sha256": "^1.3.0", "node-fetch": "^2.6.1" @@ -37625,11 +37466,11 @@ }, "packages/liveblocks-node-lexical": { "name": "@liveblocks/node-lexical", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0", + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1", "yjs": "^13.6.18" }, "devDependencies": { @@ -37646,11 +37487,11 @@ }, "packages/liveblocks-node-prosemirror": { "name": "@liveblocks/node-prosemirror", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0", + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1", "yjs": "^13.6.20" }, "devDependencies": { @@ -37670,16 +37511,17 @@ }, "packages/liveblocks-react": { "name": "@liveblocks/react", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", "@liveblocks/jest-config": "*", "@liveblocks/query-parser": "^0.1.1", + "@liveblocks/vitest-config": "*", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "14.1.2", "date-fns": "^3.6.0", @@ -37704,15 +37546,15 @@ }, "packages/liveblocks-react-blocknote": { "name": "@liveblocks/react-blocknote", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-tiptap": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-tiptap": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "@tiptap/core": "^3.19.0", "vitest-tsconfig-paths": "^3.4.1" }, @@ -37749,13 +37591,13 @@ }, "packages/liveblocks-react-flow": { "name": "@liveblocks/react-flow", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -37819,15 +37661,15 @@ }, "packages/liveblocks-react-lexical": { "name": "@liveblocks/react-lexical", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "radix-ui": "^1.4.0", "yjs": "^13.6.18" }, @@ -39461,15 +39303,15 @@ }, "packages/liveblocks-react-tiptap": { "name": "@liveblocks/react-tiptap", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "@tiptap/core": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/suggestion": "^3.19.0", @@ -41117,13 +40959,13 @@ }, "packages/liveblocks-react-ui": { "name": "@liveblocks/react-ui", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", "frimousse": "^0.2.0", "marked": "^15.0.11", "radix-ui": "^1.4.0", @@ -41497,11 +41339,11 @@ }, "packages/liveblocks-redux": { "name": "@liveblocks/redux", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", @@ -41657,11 +41499,11 @@ }, "packages/liveblocks-yjs": { "name": "@liveblocks/yjs", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", "@noble/hashes": "^1.8.0", "js-base64": "^3.7.7", "y-indexeddb": "^9.0.12" @@ -41726,11 +41568,11 @@ }, "packages/liveblocks-zustand": { "name": "@liveblocks/zustand", - "version": "3.18.0", + "version": "3.18.1", "license": "Apache-2.0", "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", diff --git a/package.json b/package.json index 3cccb68ecc4..667ccb9054d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "prettier": "^3.4.2", "publint": "^0.3.6", "ts-morph": "^22.0.0", - "tsd": "^0.31.2", "tsup": "^8.3.5", "turbo": "^2.5.6", "typescript": "^5.7.2", diff --git a/packages/liveblocks-chat-sdk-adapter/package.json b/packages/liveblocks-chat-sdk-adapter/package.json index 935496d0d0a..9222f3dda97 100644 --- a/packages/liveblocks-chat-sdk-adapter/package.json +++ b/packages/liveblocks-chat-sdk-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/chat-sdk-adapter", - "version": "3.18.0", + "version": "3.18.1", "description": "Liveblocks adapter for the Chat SDK.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -35,8 +35,8 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0" + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1" }, "peerDependencies": { "chat": ">=4.20.0" diff --git a/packages/liveblocks-client/package.json b/packages/liveblocks-client/package.json index 2a833fd55aa..cf631f835d4 100644 --- a/packages/liveblocks-client/package.json +++ b/packages/liveblocks-client/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/client", - "version": "3.18.0", + "version": "3.18.1", "description": "A client that lets you interact with Liveblocks servers. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -32,11 +32,11 @@ "lint:package": "publint --strict && attw --pack", "test": "NODE_OPTIONS=\"--no-deprecation\" vitest run --passWithNoTests", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" vitest run --passWithNoTests", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0" + "@liveblocks/core": "3.18.1" }, "devDependencies": { "@liveblocks/eslint-config": "*", diff --git a/packages/liveblocks-client/settings.json b/packages/liveblocks-client/settings.json deleted file mode 100644 index 111730cc7ca..00000000000 --- a/packages/liveblocks-client/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.tabSize": 2, - "editor.detectIndentation": false -} \ No newline at end of file diff --git a/packages/liveblocks-client/test-d/client.augmentation-only-presence.test-d.ts b/packages/liveblocks-client/test-d/client.augmentation-only-presence.test-d.ts index 5f29b709bda..fe126e6cfa4 100644 --- a/packages/liveblocks-client/test-d/client.augmentation-only-presence.test-d.ts +++ b/packages/liveblocks-client/test-d/client.augmentation-only-presence.test-d.ts @@ -1,5 +1,5 @@ import { createClient, LiveList } from "@liveblocks/client"; -import { expectError } from "tsd"; +import { describe, test } from "vitest"; type MyPresence = { cursor: { x: number; y: number }; @@ -8,27 +8,37 @@ type MyPresence = { declare global { interface Liveblocks { Presence: MyPresence; + // DO NOT ADD ANYTHING HERE! // This test file contains tests that _only_ have Presence set } } -const client = createClient({ publicApiKey: "pk_xxx" }); +describe("createClient with only Presence augmentation", () => { + describe(".enterRoom()", () => { + test("should require initial presence and allow valid calls", () => { + const client = createClient({ publicApiKey: "pk_xxx" }); -// Initial presence is required -expectError(client.enterRoom("room")); -expectError(client.enterRoom("room", {})); -expectError(client.enterRoom("room", { initialPresence: {} })); -expectError(client.enterRoom("room", { initialPresence: { foo: "" } })); -expectError(client.enterRoom("room", { initialPresence: { bar: [1, 2, 3] } })); + // @ts-expect-error - Initial presence is required + client.enterRoom("room"); + // @ts-expect-error - Initial presence is required + client.enterRoom("room", {}); + // @ts-expect-error - Initial presence is required + client.enterRoom("room", { initialPresence: {} }); + // @ts-expect-error - Invalid initial presence shape + client.enterRoom("room", { initialPresence: { foo: "" } }); + // @ts-expect-error - Invalid initial presence shape + client.enterRoom("room", { initialPresence: { bar: [1, 2, 3] } }); -// Should be fine -client.enterRoom("room", { - initialPresence: { cursor: { x: 1, y: 2 } }, -}); -client.enterRoom("room", { - initialPresence: { cursor: { x: 1, y: 2 } }, - initialStorage: { - foo: new LiveList(["I", "can", new LiveList(["be", "whatever"])]), - }, + client.enterRoom("room", { + initialPresence: { cursor: { x: 1, y: 2 } }, + }); + client.enterRoom("room", { + initialPresence: { cursor: { x: 1, y: 2 } }, + initialStorage: { + foo: new LiveList(["I", "can", new LiveList(["be", "whatever"])]), + }, + }); + }); + }); }); diff --git a/packages/liveblocks-client/test-d/client.augmentation-only-storage.test-d.ts b/packages/liveblocks-client/test-d/client.augmentation-only-storage.test-d.ts index d48e559be2e..23cc711146f 100644 --- a/packages/liveblocks-client/test-d/client.augmentation-only-storage.test-d.ts +++ b/packages/liveblocks-client/test-d/client.augmentation-only-storage.test-d.ts @@ -4,7 +4,7 @@ import { LiveMap, LiveObject, } from "@liveblocks/client"; -import { expectError } from "tsd"; +import { describe, test } from "vitest"; type MyStorage = { animals: LiveList; @@ -15,25 +15,35 @@ type MyStorage = { declare global { interface Liveblocks { Storage: MyStorage; + // DO NOT ADD ANYTHING HERE! // This test file contains tests that _only_ have Storage set } } -const client = createClient({ publicApiKey: "pk_xxx" }); +describe("createClient with only Storage augmentation", () => { + describe(".enterRoom()", () => { + test("should require initial storage and allow valid calls", () => { + const client = createClient({ publicApiKey: "pk_xxx" }); -// Initial storage is required -expectError(client.enterRoom("room")); -expectError(client.enterRoom("room", {})); -expectError(client.enterRoom("room", { initialStorage: {} })); -expectError(client.enterRoom("room", { initialStorage: { foo: "" } })); -expectError(client.enterRoom("room", { initialStorage: { bar: [1, 2, 3] } })); + // @ts-expect-error - Initial storage is required + client.enterRoom("room"); + // @ts-expect-error - Initial storage is required + client.enterRoom("room", {}); + // @ts-expect-error - Initial storage is required + client.enterRoom("room", { initialStorage: {} }); + // @ts-expect-error - Invalid initial storage shape + client.enterRoom("room", { initialStorage: { foo: "" } }); + // @ts-expect-error - Invalid initial storage shape + client.enterRoom("room", { initialStorage: { bar: [1, 2, 3] } }); -// Should be fine -client.enterRoom("room", { - initialStorage: { animals: new LiveList(["🦊"]) }, -}); -client.enterRoom("room", { - initialPresence: { can: ["be", "whatever"] }, - initialStorage: { animals: new LiveList(["🦊"]) }, + client.enterRoom("room", { + initialStorage: { animals: new LiveList(["🦊"]) }, + }); + client.enterRoom("room", { + initialPresence: { can: ["be", "whatever"] }, + initialStorage: { animals: new LiveList(["🦊"]) }, + }); + }); + }); }); diff --git a/packages/liveblocks-client/test-d/client.augmentation.test-d.ts b/packages/liveblocks-client/test-d/client.augmentation.test-d.ts index 211ce9658f1..1c6e96d6e3a 100644 --- a/packages/liveblocks-client/test-d/client.augmentation.test-d.ts +++ b/packages/liveblocks-client/test-d/client.augmentation.test-d.ts @@ -4,7 +4,7 @@ import { LiveMap, LiveObject, } from "@liveblocks/client"; -import { expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; type MyPresence = { cursor: { x: number; y: number }; @@ -25,39 +25,51 @@ declare global { const client = createClient({ publicApiKey: "pk_xxx" }); -// .enterRoom() -(async function () { - // Both initial presence + storage are required - expectError(client.enterRoom("room")); - expectError(client.enterRoom("room", {})); - expectError(client.enterRoom("room", { initialPresence: {} })); - expectError(client.enterRoom("room", { initialPresence: { foo: "" } })); - expectError( - client.enterRoom("room", { initialPresence: { bar: [1, 2, 3] } }) - ); - - // It's OK only if both are (correctly) set - const { room } = client.enterRoom("room", { - initialPresence: { cursor: { x: 1, y: 2 } }, - initialStorage: { animals: new LiveList(["🦊"]) }, +describe("createClient with both Presence and Storage augmentation", () => { + describe(".enterRoom()", () => { + test("should require both presence and storage and allow valid calls", async () => { + // @ts-expect-error - Initial presence and initial storage are required + client.enterRoom("room"); + // @ts-expect-error - Initial presence and initial storage are required + client.enterRoom("room", {}); + // @ts-expect-error - Initial presence and initial storage are required + client.enterRoom("room", { initialPresence: {} }); + // @ts-expect-error - Invalid initial presence shape + client.enterRoom("room", { initialPresence: { foo: "" } }); + // @ts-expect-error - Invalid initial presence shape + client.enterRoom("room", { initialPresence: { bar: [1, 2, 3] } }); + + client.enterRoom("room", { + initialPresence: { + cursor: { + x: 1, + // @ts-expect-error - `y` must be a number + y: "2", + }, + }, + initialStorage: { animals: new LiveList(["🦊"]) }, + }); + + client.enterRoom("room", { + initialPresence: { cursor: { x: 1, y: 2 } }, + initialStorage: { + // @ts-expect-error - `animals` must be `LiveList` + animals: new LiveList([42]), + }, + }); + }); + + test("should return typed presence and storage values", async () => { + const { room } = client.enterRoom("room", { + initialPresence: { cursor: { x: 1, y: 2 } }, + initialStorage: { animals: new LiveList(["🦊"]) }, + }); + + expectTypeOf(room.getPresence()?.cursor.x).toEqualTypeOf(); + expectTypeOf(room.getPresence()?.cursor.y).toEqualTypeOf(); + expectTypeOf((await room.getStorage()).root.get("animals")).toEqualTypeOf< + LiveList + >(); + }); }); - expectType(room.getPresence()?.cursor.x); - expectType(room.getPresence()?.cursor.y); - expectType>((await room.getStorage()).root.get("animals")); - - // Invalid presence - expectError( - client.enterRoom("room", { - initialPresence: { cursor: { x: 1, y: "2" } }, - initialStorage: { animals: new LiveList(["🦊"]) }, - }) - ); - - // Invalid storage - expectError( - client.enterRoom("room", { - initialPresence: { cursor: { x: 1, y: 2 } }, - initialStorage: { animals: new LiveList([42]) }, - }) - ); -})(); +}); diff --git a/packages/liveblocks-client/test-d/client.no-augmentation.test-d.ts b/packages/liveblocks-client/test-d/client.no-augmentation.test-d.ts index 8dd1eb3d481..c13f0e37de7 100644 --- a/packages/liveblocks-client/test-d/client.no-augmentation.test-d.ts +++ b/packages/liveblocks-client/test-d/client.no-augmentation.test-d.ts @@ -1,87 +1,87 @@ import type { Json, JsonObject } from "@liveblocks/client"; import { createClient } from "@liveblocks/client"; -import { expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; -const client = createClient({ publicApiKey: "pk_xxx" }); +describe("createClient without Liveblocks augmentation", () => { + const client = createClient({ publicApiKey: "pk_xxx" }); -// .enterRoom() -{ - { - // No options at all is fine - const { room } = client.enterRoom("my-room"); - expectType(room.getPresence()); - } + describe(".enterRoom()", () => { + test("should allow missing options when presence is generic JsonObject", () => { + { + const { room } = client.enterRoom("my-room"); + expectTypeOf(room.getPresence()).toEqualTypeOf(); + } - { - // No initial props is also fine - const { room } = client.enterRoom("my-room", { autoConnect: true }); - expectType(room.getPresence()); - } + { + const { room } = client.enterRoom("my-room", { autoConnect: true }); + expectTypeOf(room.getPresence()).toEqualTypeOf(); + } - { - // No options at all is fine if not using the global presence - const { room } = client.enterRoom("my-room", { - autoConnect: true, + { + const { room } = client.enterRoom("my-room", { + autoConnect: true, + }); + expectTypeOf(room.getPresence()).toEqualTypeOf(); + } }); - expectType(room.getPresence()); - } - { - // Initial presence is required - expectError(client.enterRoom<{ foo: string }>("room")); - expectError(client.enterRoom<{ foo: string }>("room", {})); - expectError( - client.enterRoom<{ foo: string }>("room", { initialPresence: {} }) - ); - expectError( + test("should require initial presence when Presence has required keys", () => { + // @ts-expect-error - Initial presence is required + client.enterRoom<{ foo: string }>("room"); + // @ts-expect-error - Initial presence is required + client.enterRoom<{ foo: string }>("room", {}); + // @ts-expect-error - Initial presence is required + client.enterRoom<{ foo: string }>("room", { initialPresence: {} }); client.enterRoom<{ foo: string }>("room", { + // @ts-expect-error - Invalid initial presence shape initialPresence: { bar: "" }, - }) - ); - } + }); + }); - { - // Initial presence is required... unless it's not required - client.enterRoom<{ foo?: string }>("room"); - client.enterRoom<{ foo?: string }>("room", {}); - client.enterRoom<{ foo?: string }>("room", { initialPresence: {} }); - expectError( + test("should allow omitting initial presence when all keys are optional", () => { + client.enterRoom<{ foo?: string }>("room"); + client.enterRoom<{ foo?: string }>("room", {}); + client.enterRoom<{ foo?: string }>("room", { initialPresence: {} }); client.enterRoom<{ foo?: string }>("room", { + // @ts-expect-error - Invalid initial presence shape initialPresence: { bar: "" }, - }) - ); - } - - { - // Initial presence is not required when it has only optional fields - client.enterRoom<{ foo?: string; bar?: number }>("room"); - client.enterRoom<{ foo?: string; bar?: number }>("room", {}); - client.enterRoom<{ foo?: string; bar?: number }>("room", { - initialPresence: {}, - }); - client.enterRoom<{ foo?: string; bar?: number }>("room", { - initialPresence: { foo: "" }, + }); }); - } - { - const { room } = client.enterRoom("my-room", { - initialPresence: { foo: null }, + test("should not require initial presence when only optional fields exist", () => { + client.enterRoom<{ foo?: string; bar?: number }>("room"); + client.enterRoom<{ foo?: string; bar?: number }>("room", {}); + client.enterRoom<{ foo?: string; bar?: number }>("room", { + initialPresence: {}, + }); + client.enterRoom<{ foo?: string; bar?: number }>("room", { + initialPresence: { foo: "" }, + }); }); - expectType(room.getPresence()); - } - { - const { room } = client.enterRoom("my-room", { - initialPresence: { bar: [1, 2, 3] }, - }); - expectType(room.getPresence()); - } + test("should accept JSON presence values", () => { + { + const { room } = client.enterRoom("my-room", { + initialPresence: { foo: null }, + }); + expectTypeOf(room.getPresence()).toEqualTypeOf(); + } + + { + const { room } = client.enterRoom("my-room", { + initialPresence: { bar: [1, 2, 3] }, + }); + expectTypeOf(room.getPresence()).toEqualTypeOf(); + } - { - const { room } = client.enterRoom("my-room", { - initialPresence: { cursor: { x: 1, y: 2 } }, + { + const { room } = client.enterRoom("my-room", { + initialPresence: { cursor: { x: 1, y: 2 } }, + }); + expectTypeOf(room.getPresence()?.cursor).toEqualTypeOf< + Json | undefined + >(); + } }); - expectType(room.getPresence()?.cursor); - } -} + }); +}); diff --git a/packages/liveblocks-client/test-d/room-api.test-d.ts b/packages/liveblocks-client/test-d/room-api.test-d.ts deleted file mode 100644 index 12337750a53..00000000000 --- a/packages/liveblocks-client/test-d/room-api.test-d.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { BaseMetadata } from "@liveblocks/client"; -import { createClient } from "@liveblocks/client"; -import type { - BaseUserMeta, - Json, - JsonObject, - LsonObject, - User, -} from "@liveblocks/client"; -import { expectType } from "tsd"; - -type Presence = JsonObject; -type Storage = LsonObject; -type UserMeta = BaseUserMeta; -type RoomEvent = Json; -type ThreadMetadata = BaseMetadata; -type CommentMetadata = BaseMetadata; - -type P = Presence; -type S = Storage; -type U = UserMeta; -type E = RoomEvent; -type TM = ThreadMetadata; -type CM = CommentMetadata; - -const client = createClient({ publicApiKey: "pk_whatever" }); - -// Test some Room types -const { room, leave } = client.enterRoom("my-room", { - initialPresence: {}, -}); -expectType(room.id); -expectType(leave()); - -// "Others" events through classic subscribe function -room.subscribe("others", (others, event) => { - expectType[]>(others); - switch (event.type) { - case "enter": - expectType>(event.user); - return; - case "leave": - expectType>(event.user); - return; - case "update": - expectType>(event.user); - expectType>(event.updates); - return; - case "reset": - // No extra fields on reset - return; - default: - expectType(event); - } -}); - -// "Others" events through event hub API -room.events.others.subscribe((event) => { - expectType[]>(event.others); - switch (event.type) { - case "enter": - expectType>(event.user); - return; - case "leave": - expectType>(event.user); - return; - case "update": - expectType>(event.user); - expectType>(event.updates); - return; - case "reset": - // No extra fields on reset - return; - default: - expectType(event); - } -}); diff --git a/packages/liveblocks-client/test-d/room.test-d.ts b/packages/liveblocks-client/test-d/room.test-d.ts new file mode 100644 index 00000000000..81b06e8ec19 --- /dev/null +++ b/packages/liveblocks-client/test-d/room.test-d.ts @@ -0,0 +1,99 @@ +import type { BaseMetadata } from "@liveblocks/client"; +import { createClient } from "@liveblocks/client"; +import type { + BaseUserMeta, + Json, + JsonObject, + LsonObject, + User, +} from "@liveblocks/client"; +import { describe, expectTypeOf, test } from "vitest"; + +type Presence = JsonObject; +type Storage = LsonObject; +type UserMeta = BaseUserMeta; +type RoomEvent = Json; +type ThreadMetadata = BaseMetadata; +type CommentMetadata = BaseMetadata; + +type P = Presence; +type S = Storage; +type U = UserMeta; +type E = RoomEvent; +type TM = ThreadMetadata; +type CM = CommentMetadata; + +describe("Room", () => { + describe("enterRoom", () => { + test("should expose typed room ID and leave()", () => { + const client = createClient({ publicApiKey: "pk_whatever" }); + + const { room, leave } = client.enterRoom("my-room", { + initialPresence: {}, + }); + + expectTypeOf(room.id).toEqualTypeOf(); + expectTypeOf(leave()).toEqualTypeOf(); + }); + }); + + describe('subscribe("others")', () => { + test("should narrow others event payloads", () => { + const client = createClient({ publicApiKey: "pk_whatever" }); + const { room } = client.enterRoom("my-room", { + initialPresence: {}, + }); + + room.subscribe("others", (others, event) => { + expectTypeOf(others).toEqualTypeOf[]>(); + + switch (event.type) { + case "enter": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "leave": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "update": + expectTypeOf(event.user).toEqualTypeOf>(); + expectTypeOf(event.updates).toEqualTypeOf>(); + return; + case "reset": + return; + default: + expectTypeOf(event).toEqualTypeOf(); + } + }); + }); + }); + + describe("events.others", () => { + test("should narrow others event payloads", () => { + const client = createClient({ publicApiKey: "pk_whatever" }); + const { room } = client.enterRoom("my-room", { + initialPresence: {}, + }); + + room.events.others.subscribe((event) => { + expectTypeOf(event.others).toEqualTypeOf[]>(); + + switch (event.type) { + case "enter": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "leave": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "update": + expectTypeOf(event.user).toEqualTypeOf>(); + expectTypeOf(event.updates).toEqualTypeOf>(); + return; + case "reset": + return; + default: + expectTypeOf(event).toEqualTypeOf(); + } + }); + }); + }); +}); diff --git a/packages/liveblocks-client/vitest.config.typecheck.ts b/packages/liveblocks-client/vitest.config.typecheck.ts new file mode 100644 index 00000000000..3e0a1379f7a --- /dev/null +++ b/packages/liveblocks-client/vitest.config.typecheck.ts @@ -0,0 +1,38 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +// Multiple configs are used when testing augmentation and global +// declerations, to avoid leaking globals between tests. +export default defineConfig({ + test: { + projects: [ + { + test: makeTypecheckTestConfig(import.meta, [ + "test-d/room.test-d.ts", + "test-d/client.no-augmentation.test-d.ts", + ]), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/client.augmentation.test-d.ts"], + "augmentation" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/client.augmentation-only-storage.test-d.ts"], + "augmentation-only-storage" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/client.augmentation-only-presence.test-d.ts"], + "augmentation-only-presence" + ), + }, + ], + }, +}); diff --git a/packages/liveblocks-core/package.json b/packages/liveblocks-core/package.json index 21d9b0c3720..c8bcf243bab 100644 --- a/packages/liveblocks-core/package.json +++ b/packages/liveblocks-core/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/core", - "version": "3.18.0", + "version": "3.18.1", "description": "Private internals for Liveblocks. DO NOT import directly from this package!", "type": "module", "main": "./dist/index.cjs", @@ -39,7 +39,7 @@ "lint:package": "publint --strict && attw --pack", "test": "npx liveblocks dev -p 1154 -c 'vitest run --coverage'", "test:ci": "vitest run", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest", "test:e2e": "npx liveblocks dev -p 1154 -c 'vitest run --config=./vitest.config.e2e.ts'", "test:deps": "depcruise src --exclude __tests__", diff --git a/packages/liveblocks-core/settings.json b/packages/liveblocks-core/settings.json deleted file mode 100644 index f61e6a08158..00000000000 --- a/packages/liveblocks-core/settings.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.tabSize": 2, - "editor.detectIndentation": false -} diff --git a/packages/liveblocks-core/test-d/CommentBody.test-d.ts b/packages/liveblocks-core/test-d/CommentBody.test-d.ts index 9e9a19d0d2d..7448ab67b00 100644 --- a/packages/liveblocks-core/test-d/CommentBody.test-d.ts +++ b/packages/liveblocks-core/test-d/CommentBody.test-d.ts @@ -1,296 +1,307 @@ -/* eslint-disable */ - import type { CommentBody } from "@liveblocks/core"; -import { expectAssignable, expectNotAssignable } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; -// ❌ Invalid required properties -{ - expectNotAssignable({}); - expectNotAssignable({ version: 1 }); - expectNotAssignable({ content: [] }); - expectNotAssignable({ version: 2, content: [] }); - expectNotAssignable({ version: "1", content: [] }); - expectNotAssignable({ version: 1, content: "text" }); - expectNotAssignable({ version: 1, content: {} }); - expectNotAssignable({ - version: 1, - content: [{ type: "invalid" }], +describe("CommentBody", () => { + test("should reject invalid required properties", () => { + // @ts-expect-error - `version` and `content` are required + ({}) satisfies CommentBody; + // @ts-expect-error - `content` is required + ({ version: 1 }) satisfies CommentBody; + // @ts-expect-error - `version` is required + ({ content: [] }) satisfies CommentBody; + // @ts-expect-error - `version` must be 1 + ({ version: 2, content: [] }) satisfies CommentBody; + // @ts-expect-error - `version` must be a number + ({ version: "1", content: [] }) satisfies CommentBody; + // @ts-expect-error - `content` must be an array + ({ version: 1, content: "text" }) satisfies CommentBody; + // @ts-expect-error - `content` must be an array + ({ version: 1, content: {} }) satisfies CommentBody; + // @ts-expect-error - `content` must be an array of valid block elements + ({ version: 1, content: [{ type: "invalid" }] }) satisfies CommentBody; }); -} -// ❌ Invalid block elements -{ - // Invalid paragraph element - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph" }], - }); - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph", children: "text" }], - }); - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph", children: [{}] }], - }); -} + test("should reject invalid block elements", () => { + /** + * Invalid paragraph element + */ -// ❌ Invalid inline elements -{ - // Invalid text element - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph", children: [{ text: 123 }] }], - }); - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph", children: [{ bold: true }] }], + // @ts-expect-error - `children` is required + ({ version: 1, content: [{ type: "paragraph" }] }) satisfies CommentBody; + ({ + version: 1, + // @ts-expect-error - `children` must be an array + content: [{ type: "paragraph", children: "text" }], + }) satisfies CommentBody; + ({ + version: 1, + // @ts-expect-error - `children` must be an array of valid inline elements + content: [{ type: "paragraph", children: [{}] }], + }) satisfies CommentBody; }); - // Invalid mention element - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "mention" }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "mention", id: 123 }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "mention", kind: "invalid", id: "user1" }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "mention", kind: "user", id: "user1", userIds: [] }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - // Missing `type: "mention"` so it's a text element - children: [{ text: "mention", kind: "user", id: "user123" }], - }, - ], - }); + test("should reject invalid inline elements", () => { + /** + * Invalid text element + */ - // Invalid link element - expectNotAssignable({ - version: 1, - content: [{ type: "paragraph", children: [{ type: "link" }] }], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "link", url: 123 }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "link", url: "https://liveblocks.io", text: 123 }], - }, - ], - }); - expectNotAssignable({ - version: 1, - content: [ - { - type: "paragraph", - // Missing `type: "link"` so it's a text element - children: [{ text: "link elsewhere", url: "https://liveblocks.io" }], - }, - ], - }); -} + ({ + version: 1, + // @ts-expect-error - `text` must be a string + content: [{ type: "paragraph", children: [{ text: 123 }] }], + }) satisfies CommentBody; + ({ + version: 1, + // @ts-expect-error - text elements must include `text` + content: [{ type: "paragraph", children: [{ bold: true }] }], + }) satisfies CommentBody; -// ✅ Valid comment bodies -{ - expectAssignable({ - version: 1, - content: [], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ text: "Hello world" }], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { - text: "Hello", - bold: true, - italic: true, - strikethrough: false, - code: false, - }, - ], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "mention", kind: "user", id: "user123" }], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { - type: "mention", - kind: "group", - id: "group123", - }, - ], - }, - ], - }); + /** + * Invalid mention element + */ - // Paragraph with group mention including userIds - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { - type: "mention", - kind: "group", - id: "group123", - userIds: ["user1", "user2"], - }, - ], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ type: "link", url: "https://liveblocks.io" }], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { - type: "link", - url: "https://liveblocks.io", - text: "Liveblocks", - }, - ], - }, - ], - }); + ({ + version: 1, + // @ts-expect-error - `kind` and `id` are required + content: [{ type: "paragraph", children: [{ type: "mention" }] }], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + // @ts-expect-error - mention nodes require `kind`, and `id` must be a string + { type: "paragraph", children: [{ type: "mention", id: 123 }] }, + ], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + { + type: "paragraph", + // @ts-expect-error - `kind` must be either "user" or "group" + children: [{ type: "mention", kind: "invalid", id: "user1" }], + }, + ], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + // @ts-expect-error - `userIds` can only be set if `kind` is "group" + { type: "mention", kind: "user", id: "user1", userIds: [] }, + ], + }, + ], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + { + type: "paragraph", + // @ts-expect-error - Typo of `text: "mention"` instead of `type: "mention"` so it's a text element (which can't have `kind` and `id`) + children: [{ text: "mention", kind: "user", id: "user123" }], + }, + ], + }) satisfies CommentBody; - // Multiple paragraphs - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [{ text: "First paragraph" }], - }, - { - type: "paragraph", - children: [{ text: "Second paragraph" }], - }, - ], - }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { text: "Hey " }, - { type: "mention", kind: "user", id: "user123" }, - { text: ", can you look at " }, - { - type: "link", - url: "https://liveblocks.io", - text: "this link", - }, - { text: "?" }, - ], - }, - ], + /** + * Invalid link element + */ + + ({ + version: 1, + // @ts-expect-error - `url` is required + content: [{ type: "paragraph", children: [{ type: "link" }] }], + }) satisfies CommentBody; + ({ + version: 1, + // @ts-expect-error - `url` must be a string + content: [{ type: "paragraph", children: [{ type: "link", url: 123 }] }], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + { + type: "paragraph", + // @ts-expect-error - `text` must be a string + children: [{ type: "link", url: "https://liveblocks.io", text: 123 }], + }, + ], + }) satisfies CommentBody; + ({ + version: 1, + content: [ + { + type: "paragraph", + // @ts-expect-error - Typo of `text: "link"` instead of `type: "link"` so it's a text element (which can't have `url`) + children: [{ text: "link", url: "https://liveblocks.io" }], + }, + ], + }) satisfies CommentBody; }); - expectAssignable({ - version: 1, - content: [ - { - type: "paragraph", - children: [ - { text: "Bold text", bold: true }, - { text: " and " }, - { text: "italic text", italic: true }, - ], - }, - { - type: "paragraph", - children: [ - { text: "Code: ", code: true }, - { text: "const x = 1;", code: true }, - ], - }, - { - type: "paragraph", - children: [ - { - type: "mention", - kind: "group", - id: "team1", - userIds: ["user1", "user2", "user3"], - }, - ], - }, - ], + + test("should accept valid comment bodies", () => { + expectTypeOf({ + version: 1, + content: [], + } satisfies CommentBody).toExtend(); + + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [{ text: "Hello world" }], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { + text: "Hello", + bold: true, + italic: true, + strikethrough: false, + code: false, + }, + ], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [{ type: "mention", kind: "user", id: "user123" }], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { + type: "mention", + kind: "group", + id: "group123", + }, + ], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { + type: "mention", + kind: "group", + id: "group123", + userIds: ["user1", "user2"], + }, + ], + }, + ], + } satisfies CommentBody).toExtend(); + + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [{ type: "link", url: "https://liveblocks.io" }], + }, + ], + } satisfies CommentBody).toExtend(); + + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { + type: "link", + url: "https://liveblocks.io", + text: "Liveblocks", + }, + ], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [{ text: "First paragraph" }], + }, + { + type: "paragraph", + children: [{ text: "Second paragraph" }], + }, + ], + } satisfies CommentBody).toExtend(); + + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { text: "Hey " }, + { type: "mention", kind: "user", id: "user123" }, + { text: ", can you look at " }, + { + type: "link", + url: "https://liveblocks.io", + text: "this link", + }, + { text: "?" }, + ], + }, + ], + } satisfies CommentBody).toExtend(); + expectTypeOf({ + version: 1, + content: [ + { + type: "paragraph", + children: [ + { text: "Bold text", bold: true }, + { text: " and " }, + { text: "italic text", italic: true }, + ], + }, + { + type: "paragraph", + children: [ + { text: "Code: ", code: true }, + { text: "const x = 1;", code: true }, + ], + }, + { + type: "paragraph", + children: [ + { + type: "mention", + kind: "group", + id: "team1", + userIds: ["user1", "user2", "user3"], + }, + ], + }, + ], + } satisfies CommentBody).toExtend(); }); -} +}); diff --git a/packages/liveblocks-core/test-d/EnsureJson.test-d.ts b/packages/liveblocks-core/test-d/EnsureJson.test-d.ts index 4997d3229dc..81ad888c73b 100644 --- a/packages/liveblocks-core/test-d/EnsureJson.test-d.ts +++ b/packages/liveblocks-core/test-d/EnsureJson.test-d.ts @@ -1,5 +1,5 @@ import type { EnsureJson, Json, JsonObject } from "@liveblocks/core"; -import { expectAssignable, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; type Item = { n?: number; @@ -50,18 +50,29 @@ declare const uo: EnsureJson<{ toString(): string; }>; -expectType(u); -expectType(ua1); -expectType<(number | string)[]>(ua2); -expectType<(number | string | boolean | null)[]>(ua3); -expectType(ua4); -expectType(ua5); +describe("EnsureJson", () => { + test("should limit unknown types to JSON", () => { + expectTypeOf(u).toEqualTypeOf(); + }); -expectType<{ hi?: Json; date?: number | string }>(uo); -expectAssignable(uo); + test("should keep JSON-compatible types", () => { + expectTypeOf(ua1).toEqualTypeOf(); + expectTypeOf(ua2).toEqualTypeOf<(number | string)[]>(); + expectTypeOf(ua3).toEqualTypeOf<(number | string | boolean | null)[]>(); + expectTypeOf(ua4).toEqualTypeOf(); + expectTypeOf(ua5).toEqualTypeOf(); + }); -expectType<{ - createdAt?: string | number; // Date became a string - err: { name: string; message: string; stack?: string; cause?: Json }; - items: Item[]; -}>(u6); + test("should strip non-JSON members and method signatures", () => { + expectTypeOf(uo).toEqualTypeOf<{ hi?: Json; date?: number | string }>(); + expectTypeOf(uo).toExtend(); + }); + + test("should convert non-JSON types to JSON", () => { + expectTypeOf(u6).toEqualTypeOf<{ + createdAt?: string | number; // Date became a string + err: { name: string; message: string; stack?: string; cause?: Json }; // Error became a plain object + items: Item[]; + }>(); + }); +}); diff --git a/packages/liveblocks-core/test-d/InferFromSchema.test-d.ts b/packages/liveblocks-core/test-d/InferFromSchema.test-d.ts index 6cf71e52e6a..a79cba62896 100644 --- a/packages/liveblocks-core/test-d/InferFromSchema.test-d.ts +++ b/packages/liveblocks-core/test-d/InferFromSchema.test-d.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ - import type { AiOpaqueToolDefinition, Awaitable, @@ -11,749 +9,780 @@ import type { } from "@liveblocks/core"; import { defineAiTool, kInternal } from "@liveblocks/core"; import type { JSONSchema7 } from "json-schema"; -import { expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; function infer(x: T): InferFromSchema { return x as any; } -// Object tests -{ - expectType<{ - name: string; - age: number; - hobbies?: string[]; - }>( - infer({ - type: "object", - properties: { - name: { type: "string" }, - age: { type: "number" }, - hobbies: { - type: "array", - items: { type: "string" }, +describe("InferFromSchema", () => { + test("should infer object types with required and optional properties", () => { + expectTypeOf( + infer({ + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + hobbies: { + type: "array", + items: { type: "string" }, + }, }, - }, - required: ["name", "age"], - additionalProperties: false, - }) - ); - - expectType<{ - ids: number[]; - }>( - infer({ - type: "object", - properties: { - ids: { - type: "array", - description: "The requested todo items to list", - items: { type: "number" }, + required: ["name", "age"], + additionalProperties: false, + }) + ).toEqualTypeOf<{ + name: string; + age: number; + hobbies?: string[]; + }>(); + + expectTypeOf( + infer({ + type: "object", + properties: { + ids: { + type: "array", + description: "The requested todo items to list", + items: { type: "number" }, + }, }, - }, - required: ["ids"], - additionalProperties: false, - }) - ); -} + required: ["ids"], + additionalProperties: false, + }) + ).toEqualTypeOf<{ + ids: number[]; + }>(); + }); -// Unions (XOR) -{ - expectType( - infer({ - oneOf: [{ type: "string" }, { type: "number" }], - }) - ); - - expectType<{ name: string } | { age: number }>( - infer({ - oneOf: [ - { - type: "object", - properties: { name: { type: "string" } }, - required: ["name"], - additionalProperties: false, - }, - { - type: "object", - properties: { age: { type: "number" } }, - required: ["age"], - additionalProperties: false, - }, - ], - }) - ); - - expectType<"success" | "error" | { code: number }>( - infer({ - oneOf: [ - { type: "string", enum: ["success", "error"] }, - { - type: "object", - properties: { code: { type: "number" } }, - required: ["code"], - additionalProperties: false, + test("should infer oneOf (XOR) as a union", () => { + expectTypeOf( + infer({ + oneOf: [{ type: "string" }, { type: "number" }], + }) + ).toEqualTypeOf(); + + expectTypeOf( + infer({ + oneOf: [ + { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }, + { + type: "object", + properties: { age: { type: "number" } }, + required: ["age"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf<{ name: string } | { age: number }>(); + + expectTypeOf( + infer({ + oneOf: [ + { type: "string", enum: ["success", "error"] }, + { + type: "object", + properties: { code: { type: "number" } }, + required: ["code"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf<"success" | "error" | { code: number }>(); + + expectTypeOf( + infer({ + type: "array", + items: { + oneOf: [{ type: "number" }, { type: "null" }], }, - ], - }) - ); - - expectType<(number | null)[]>( - infer({ - type: "array", - items: { - oneOf: [{ type: "number" }, { type: "null" }], - }, - }) - ); + }) + ).toEqualTypeOf<(number | null)[]>(); - // Arrays without a subschema - expectType(infer({ type: "array" })); -} + // Arrays without a subschema + expectTypeOf(infer({ type: "array" })).toEqualTypeOf(); + }); -// Unions (OR) -{ - expectType( - infer({ - anyOf: [{ type: "string" }, { type: "number" }], - }) - ); - - expectType<{ name: string } | { age: number }>( - infer({ - anyOf: [ - { - type: "object", - properties: { name: { type: "string" } }, - required: ["name"], - additionalProperties: false, - }, - { - type: "object", - properties: { age: { type: "number" } }, - required: ["age"], - additionalProperties: false, - }, - ], - }) - ); - - expectType<"pending" | "approved" | { details: string }>( - infer({ - anyOf: [ - { type: "string", enum: ["pending", "approved"] }, - { - type: "object", - properties: { details: { type: "string" } }, - required: ["details"], - additionalProperties: false, - }, - ], - }) - ); -} + test("should infer anyOf (OR) as a union", () => { + expectTypeOf( + infer({ + anyOf: [{ type: "string" }, { type: "number" }], + }) + ).toEqualTypeOf(); + + expectTypeOf( + infer({ + anyOf: [ + { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }, + { + type: "object", + properties: { age: { type: "number" } }, + required: ["age"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf<{ name: string } | { age: number }>(); + + expectTypeOf( + infer({ + anyOf: [ + { type: "string", enum: ["pending", "approved"] }, + { + type: "object", + properties: { details: { type: "string" } }, + required: ["details"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf<"pending" | "approved" | { details: string }>(); + }); -// Intersections (AND) -{ - expectType<{ name: string } & { age: number }>( - infer({ - allOf: [ - { + test("should infer allOf (AND) as an intersection", () => { + expectTypeOf( + infer({ + allOf: [ + { + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }, + { + type: "object", + properties: { age: { type: "number" } }, + required: ["age"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf<{ name: string } & { age: number }>(); + + expectTypeOf( + infer({ + allOf: [ + { + type: "object", + properties: { id: { type: "number" } }, + required: ["id"], + additionalProperties: false, + }, + { + type: "object", + properties: { title: { type: "string" } }, + required: ["title"], + additionalProperties: false, + }, + { + type: "object", + properties: { completed: { type: "boolean" } }, + required: ["completed"], + additionalProperties: false, + }, + ], + }) + ).toEqualTypeOf< + { id: number } & { title: string } & { completed: boolean } + >(); + }); + + test("should infer not as JSON (no meaningful narrowing)", () => { + expectTypeOf(infer({ not: { type: "string" } })).toEqualTypeOf(); + expectTypeOf( + infer({ not: { const: "forbidden-value" } }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + not: { type: "object", properties: { name: { type: "string" } }, required: ["name"], additionalProperties: false, }, - { - type: "object", - properties: { age: { type: "number" } }, - required: ["age"], - additionalProperties: false, - }, - ], - }) - ); - - expectType<{ id: number } & { title: string } & { completed: boolean }>( - infer({ - allOf: [ - { - type: "object", - properties: { id: { type: "number" } }, - required: ["id"], - additionalProperties: false, - }, - { - type: "object", - properties: { title: { type: "string" } }, - required: ["title"], - additionalProperties: false, - }, - { - type: "object", - properties: { completed: { type: "boolean" } }, - required: ["completed"], - additionalProperties: false, - }, - ], - }) - ); -} + }) + ).toEqualTypeOf(); + }); + + test("should infer enum types as literal unions", () => { + expectTypeOf( + infer({ + enum: ["active", "inactive", null], + }) + ).toEqualTypeOf<"active" | "inactive" | null>(); -// Not (not super meaningful inference outputs) -{ - expectType(infer({ not: { type: "string" } })); - expectType(infer({ not: { const: "forbidden-value" } })); - expectType( - infer({ - not: { + expectTypeOf( + infer({ type: "object", - properties: { name: { type: "string" } }, - required: ["name"], + properties: { + mode: { type: "string", enum: ["dark", "light"] }, + favNumber: { type: "number", enum: [3, 7, 13, 42] }, + }, + required: ["mode", "favNumber"], additionalProperties: false, - }, - }) - ); -} - -// Enums -{ - expectType<"active" | "inactive" | null>( - infer({ - enum: ["active", "inactive", null], - }) - ); -} - -{ - expectType<{ - mode: "dark" | "light"; - favNumber: 3 | 7 | 13 | 42; - }>( - infer({ - type: "object", - properties: { - mode: { type: "string", enum: ["dark", "light"] }, - favNumber: { type: "number", enum: [3, 7, 13, 42] }, - }, - required: ["mode", "favNumber"], - additionalProperties: false, - }) - ); -} - -// Constants -{ - expectType<"exactly-this-value">(infer({ const: "exactly-this-value" })); - expectType<42>(infer({ const: 42 })); - expectType(infer({ const: null })); - expectType(infer({ const: true })); -} - -// Various string validations (these all "just" infer as string) -{ - expectType(infer({ type: "string", pattern: "^[a-zA-Z0-9]+$" })); - expectType(infer({ type: "string", minLength: 5, maxLength: 20 })); - expectType(infer({ type: "string", format: "email" })); - expectType(infer({ type: "string", format: "uri" })); - expectType(infer({ type: "string", format: "uuid" })); - expectType(infer({ type: "string", format: "date" })); - expectType(infer({ type: "string", format: "date-time" })); - expectType(infer({ type: "string", format: "ipv4" })); - expectType(infer({ type: "string", format: "ipv6" })); - expectType( - infer({ - type: "string", - pattern: "^[A-Z][a-z]+$", - minLength: 2, - maxLength: 50, - format: "hostname", - }) - ); - expectType(infer({ type: "string", format: "custom-format" })); - expectType( - infer({ - type: "string", - description: "A descriptive string field", - pattern: ".*", - }) - ); -} - -// Various number validations (these all "just" infer as number) -{ - expectType(infer({ type: "number", minimum: 0 })); - expectType(infer({ type: "number", maximum: 100 })); - expectType(infer({ type: "number", minimum: 0, maximum: 100 })); - expectType(infer({ type: "number", exclusiveMinimum: 0 })); - expectType(infer({ type: "number", exclusiveMaximum: 100 })); - expectType(infer({ type: "number", multipleOf: 5 })); - expectType( - infer({ - type: "number", - minimum: 1, - maximum: 1000, - multipleOf: 10, - description: "A constrained number", - }) - ); - - // Integer with constraints - expectType(infer({ type: "integer" })); - expectType(infer({ type: "integer", minimum: 1, maximum: 10 })); - expectType(infer({ type: "integer", multipleOf: 2 })); -} - -// Various boolean validations (these all "just" infer as boolean) -{ - expectType(infer({ type: "boolean" })); - expectType( - infer({ - type: "boolean", - description: "A boolean flag", - title: "Enable feature", - }) - ); - expectType(infer({ type: "boolean", default: true })); -} - -// Various array validations (these all "just" infer as T[]) -{ - expectType( - infer({ type: "array", items: { type: "string" }, minItems: 1 }) - ); - expectType( - infer({ type: "array", items: { type: "number" }, maxItems: 10 }) - ); - expectType( - infer({ - type: "array", - items: { type: "string" }, - minItems: 2, - maxItems: 5, - }) - ); - expectType( - infer({ type: "array", items: { type: "boolean" }, uniqueItems: true }) - ); - expectType<(string | number)[]>( - infer({ - type: "array", - items: { oneOf: [{ type: "string" }, { type: "number" }] }, - minItems: 0, - maxItems: 100, - uniqueItems: false, - description: "Mixed type array with constraints", - }) - ); -} + }) + ).toEqualTypeOf<{ + mode: "dark" | "light"; + favNumber: 3 | 7 | 13 | 42; + }>(); + }); -{ - defineAiTool()({ - description: "List all todos", - parameters: { - type: "object", - properties: { - ids: { - type: "array", - description: "The requested todo items to list", - items: { type: "number" }, - }, - }, - required: ["ids"], - additionalProperties: false, - }, - execute: async (args) => { - expectType<{ ids: number[] }>(args); - return { data: { ok: true } }; - }, + test("should infer const values as literal types", () => { + expectTypeOf( + infer({ const: "exactly-this-value" }) + ).toEqualTypeOf<"exactly-this-value">(); + expectTypeOf(infer({ const: 42 })).toEqualTypeOf<42>(); + expectTypeOf(infer({ const: null })).toEqualTypeOf(); + expectTypeOf(infer({ const: true })).toEqualTypeOf(); }); -} -{ - defineAiTool()({ - description: "Add a new todo item to the list", - parameters: { - type: "object", - properties: { - titles: { - type: "array", - description: "The titles of the new items to add to the list", - items: { type: "string" }, - }, - }, - required: ["titles"], - additionalProperties: false, - }, - execute: (args) => { - expectType<{ titles: string[] }>(args); - return { data: { ok: true } }; - }, + test("should infer string constraints as string", () => { + expectTypeOf( + infer({ type: "string", pattern: "^[a-zA-Z0-9]+$" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", minLength: 5, maxLength: 20 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "email" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "uri" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "uuid" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "date" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "date-time" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "ipv4" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "ipv6" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "string", + pattern: "^[A-Z][a-z]+$", + minLength: 2, + maxLength: 50, + format: "hostname", + }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "string", format: "custom-format" }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "string", + description: "A descriptive string field", + pattern: ".*", + }) + ).toEqualTypeOf(); }); -} -{ - defineAiTool()({ - description: "Toggle a todo's completion status", - parameters: { - type: "object", - properties: { - id: { - description: "The id of the todo to toggle", - type: "number", - }, - }, - // required: ["id"], - additionalProperties: false, - }, - execute: (args) => { - expectType<{ id?: number }>(args); - return { data: { ok: true } }; - }, - render: () => null, + test("should infer number/integer constraints as number", () => { + expectTypeOf(infer({ type: "number", minimum: 0 })).toEqualTypeOf(); + expectTypeOf( + infer({ type: "number", maximum: 100 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "number", minimum: 0, maximum: 100 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "number", exclusiveMinimum: 0 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "number", exclusiveMaximum: 100 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "number", multipleOf: 5 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "number", + minimum: 1, + maximum: 1000, + multipleOf: 10, + description: "A constrained number", + }) + ).toEqualTypeOf(); + + expectTypeOf(infer({ type: "integer" })).toEqualTypeOf(); + expectTypeOf( + infer({ type: "integer", minimum: 1, maximum: 10 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "integer", multipleOf: 2 }) + ).toEqualTypeOf(); }); -} -{ - defineAiTool()({ - description: "Toggle a todo's completion status", - parameters: { - type: "object", - properties: { id: { type: "number" } }, - required: ["id"], - additionalProperties: false, - }, - execute: (args) => { - expectType<{ id: number }>(args); - return { data: { ok: true } }; - }, - render: ({ args, partialArgs }) => { - if (partialArgs !== undefined) { - expectType(args); - } else { - expectType<{ id: number }>(args); - } - return null; - }, + test("should infer boolean constraints as boolean", () => { + expectTypeOf(infer({ type: "boolean" })).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "boolean", + description: "A boolean flag", + title: "Enable feature", + }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "boolean", default: true }) + ).toEqualTypeOf(); }); -} -{ - // Resulting tool definition is opaque - const myTool = defineAiTool()({ - description: "First tool", - parameters: { - type: "object", - properties: { - foo: { type: "number" }, - }, - additionalProperties: false, - }, - execute: (args) => { - expectType<{ foo?: number }>(args); - return { data: { ok: true } }; - }, + test("should infer array constraints as T[]", () => { + expectTypeOf( + infer({ type: "array", items: { type: "string" }, minItems: 1 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "array", items: { type: "number" }, maxItems: 10 }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "array", + items: { type: "string" }, + minItems: 2, + maxItems: 5, + }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ type: "array", items: { type: "boolean" }, uniqueItems: true }) + ).toEqualTypeOf(); + expectTypeOf( + infer({ + type: "array", + items: { oneOf: [{ type: "string" }, { type: "number" }] }, + minItems: 0, + maxItems: 100, + uniqueItems: false, + description: "Mixed type array with constraints", + }) + ).toEqualTypeOf<(string | number)[]>(); }); - expectType(myTool.description); - if (myTool.execute) { - expectType< - ( - args: JsonObject, - context: { name: string; invocationId: string } - ) => Awaitable - >(myTool.execute); - } else { - expectType(myTool.execute); - } - if (!myTool.render) { - expectType(myTool.render); - } else { - const internal = { - [kInternal]: { execute: undefined, messageStatus: "generating" as const }, - }; - - // Possible JSX rendering invocation 1 - myTool.render({ - stage: "receiving", - name: "callMyTool", - invocationId: "tc_abc123", - partialArgs: {}, - respond: (payload) => { - expectType(payload); - }, - types: undefined as never, - ...internal, - }); + test("should infer additionalProperties variants correctly", () => { + // No additionalProperties (defaults to true) + expectTypeOf( + infer({ + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + }) + ).toEqualTypeOf<{ + name: string; + [extra: string]: Json | undefined; + }>(); + expectTypeOf( + infer({ + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: true, + }) + ).toEqualTypeOf<{ + name: string; + [extra: string]: Json | undefined; + }>(); + + // When additionalProperties: false + expectTypeOf( + infer({ + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: false, + }) + ).toEqualTypeOf<{ + name: string; + }>(); + + // "Empty" object variations + expectTypeOf(infer({ type: "object" })).toEqualTypeOf<{ + [extra: string]: Json | undefined; + }>(); + expectTypeOf( + infer({ type: "object", additionalProperties: undefined }) + ).toEqualTypeOf<{ [extra: string]: Json | undefined }>(); + expectTypeOf( + infer({ type: "object", additionalProperties: true }) + ).toEqualTypeOf<{ [extra: string]: Json | undefined }>(); + // TODO: Make this Record again later? + expectTypeOf( + infer({ type: "object", additionalProperties: false }) + ).toEqualTypeOf<{}>(); + + // When additionalProperties with custom schema + expectTypeOf( + infer({ + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], + additionalProperties: { + oneOf: [{ type: "string" }, { type: "boolean" }, { type: "number" }], + }, + }) + ).toEqualTypeOf<{ + name: string; + [extra: string]: string | number | boolean | undefined; + }>(); + + // Nested additionalProperties with complex schema + expectTypeOf( + infer({ + type: "object", + additionalProperties: { + type: "object", + properties: { + id: { type: "number" }, + label: { type: "string" }, + }, + required: ["id", "label"], + additionalProperties: false, + }, + }) + ).toEqualTypeOf<{ + [extra: string]: { id: number; label: string } | undefined; + }>(); + }); +}); - // Possible JSX rendering invocation 2 - myTool.render({ - stage: "executing", - name: "callMyTool", - invocationId: "tc_abc123", - args: { a: 1 }, - respond: (payload) => { - expectType(payload); +describe("defineAiTool", () => { + test("should infer args type from parameters schema", () => { + defineAiTool()({ + description: "List all todos", + parameters: { + type: "object", + properties: { + ids: { + type: "array", + description: "The requested todo items to list", + items: { type: "number" }, + }, + }, + required: ["ids"], + additionalProperties: false, }, - types: undefined as never, - ...internal, - }); - - // Possible JSX rendering invocation 3 - myTool.render({ - stage: "executed", - name: "callMyTool", - invocationId: "tc_abc123", - args: { a: 1 }, - result: { type: "success", data: { b: 2 } }, - respond: (payload) => { - expectType(payload); + execute: async (args) => { + expectTypeOf(args).toEqualTypeOf<{ ids: number[] }>(); + return { data: { ok: true } }; }, - types: undefined as never, - ...internal, }); - } -} -{ - // This tests that tool definitions will get locally inferred, not overridden - // by the array's *opaque* type they are getting assigned into! - const tools: AiOpaqueToolDefinition[] = [ defineAiTool()({ - description: "First tool (execute, no render)", + description: "Add a new todo item to the list", parameters: { type: "object", - properties: { foo: { type: "number" } }, + properties: { + titles: { + type: "array", + description: "The titles of the new items to add to the list", + items: { type: "string" }, + }, + }, + required: ["titles"], additionalProperties: false, }, execute: (args) => { - expectType<{ foo?: number }>(args); + expectTypeOf(args).toEqualTypeOf<{ titles: string[] }>(); return { data: { ok: true } }; }, - }), + }); defineAiTool()({ - description: "Second tool (execute, no render)", + description: "Toggle a todo's completion status", parameters: { type: "object", - properties: { bar: { type: "string" } }, + properties: { + id: { + description: "The id of the todo to toggle", + type: "number", + }, + }, + // required: ["id"], additionalProperties: false, }, execute: (args) => { - expectType<{ bar?: string }>(args); + expectTypeOf(args).toEqualTypeOf<{ id?: number }>(); return { data: { ok: true } }; }, - }), + render: () => null, + }); + }); + test("should narrow args/partialArgs in render callback based on stage", () => { defineAiTool()({ - description: "Third tool (execute & render)", + description: "Toggle a todo's completion status", parameters: { type: "object", - properties: { bar: { type: "string" } }, + properties: { id: { type: "number" } }, + required: ["id"], additionalProperties: false, }, execute: (args) => { - expectType<{ bar?: string }>(args); + expectTypeOf(args).toEqualTypeOf<{ id: number }>(); return { data: { ok: true } }; }, - render: ({ stage, partialArgs, args, result, respond }) => { - expectType<(payload?: ToolResultResponse) => void>(respond); - - expectType<"receiving" | "executing" | "executed">(stage); - if (stage === "receiving") { - expectType(partialArgs); - expectType(args); - expectType(result); - } else if (stage === "executing") { - expectType(partialArgs); - expectType<{ bar?: string }>(args); - expectType(result); + render: ({ args, partialArgs }) => { + if (partialArgs !== undefined) { + expectTypeOf(args).toEqualTypeOf(); } else { - expectType(partialArgs); - expectType<{ bar?: string }>(args); - expectType(result); + expectTypeOf(args).toEqualTypeOf<{ id: number }>(); } return null; }, - }), + }); + }); - defineAiTool()({ - description: "Fourth tool (render, no execute)", + test("should produce an opaque tool definition", () => { + const myTool = defineAiTool()({ + description: "First tool", parameters: { type: "object", - properties: { bar: { type: "string" } }, + properties: { + foo: { type: "number" }, + }, additionalProperties: false, }, - render: ({ stage, partialArgs, args, result, respond }) => { - expectType<(payload?: ToolResultResponse) => void>(respond); - - expectType<"receiving" | "executing" | "executed">(stage); - if (stage === "receiving") { - expectType(partialArgs); - expectType(args); - expectType(result); - } else if (stage === "executing") { - expectType(partialArgs); - expectType<{ bar?: string }>(args); - expectType(result); - } else { - expectType(partialArgs); - expectType<{ bar?: string }>(args); - expectType(result); - } - return null; + execute: (args) => { + expectTypeOf(args).toEqualTypeOf<{ foo?: number }>(); + return { data: { ok: true } }; }, - }), - ]; - console.log(tools); // Just use the tools variable -} + }); -// Objects with additionalProperties field -{ - // No additionalProperties (defaults to true) - expectType<{ - name: string; - [extra: string]: Json | undefined; - }>( - infer({ - type: "object", - properties: { - name: { type: "string" }, - }, - required: ["name"], - }) - ); - expectType<{ - name: string; - [extra: string]: Json | undefined; - }>( - infer({ - type: "object", - properties: { - name: { type: "string" }, - }, - required: ["name"], - additionalProperties: true, - }) - ); - - // When additionalProperties: false - expectType<{ - name: string; - }>( - infer({ - type: "object", - properties: { - name: { type: "string" }, - }, - required: ["name"], - additionalProperties: false, - }) - ); - - // "Empty" object variations - expectType<{ [extra: string]: Json | undefined }>(infer({ type: "object" })); - expectType<{ [extra: string]: Json | undefined }>( - infer({ type: "object", additionalProperties: undefined }) - ); - expectType<{ [extra: string]: Json | undefined }>( - infer({ type: "object", additionalProperties: true }) - ); - // TODO Make this Record again later? - expectType<{}>(infer({ type: "object", additionalProperties: false })); - - // When additionalProperties with custom schema - expectType<{ - name: string; - [extra: string]: string | number | boolean | undefined; - }>( - infer({ - type: "object", - properties: { name: { type: "string" } }, - required: ["name"], - additionalProperties: { - oneOf: [{ type: "string" }, { type: "boolean" }, { type: "number" }], - }, - }) - ); - - // Nested additionalProperties with complex schema - expectType<{ - [extra: string]: { id: number; label: string } | undefined; - }>( - infer({ - type: "object", - additionalProperties: { - type: "object", - properties: { - id: { type: "number" }, - label: { type: "string" }, + expectTypeOf(myTool.description).toEqualTypeOf(); + if (myTool.execute) { + expectTypeOf(myTool.execute).toEqualTypeOf< + ( + args: JsonObject, + context: { name: string; invocationId: string } + ) => Awaitable + >(); + } else { + expectTypeOf(myTool.execute).toEqualTypeOf(); + } + if (!myTool.render) { + expectTypeOf(myTool.render).toEqualTypeOf(); + } else { + const internal = { + [kInternal]: { + execute: undefined, + messageStatus: "generating" as const, }, - required: ["id", "label"], - additionalProperties: false, - }, - }) - ); -} + }; + + // Possible JSX rendering invocation 1 + myTool.render({ + stage: "receiving", + name: "callMyTool", + invocationId: "tc_abc123", + partialArgs: {}, + respond: (payload) => { + expectTypeOf(payload).toEqualTypeOf(); + }, + types: undefined as never, + ...internal, + }); + + // Possible JSX rendering invocation 2 + myTool.render({ + stage: "executing", + name: "callMyTool", + invocationId: "tc_abc123", + args: { a: 1 }, + respond: (payload) => { + expectTypeOf(payload).toEqualTypeOf(); + }, + types: undefined as never, + ...internal, + }); + + // Possible JSX rendering invocation 3 + myTool.render({ + stage: "executed", + name: "callMyTool", + invocationId: "tc_abc123", + args: { a: 1 }, + result: { type: "success", data: { b: 2 } }, + respond: (payload) => { + expectTypeOf(payload).toEqualTypeOf(); + }, + types: undefined as never, + ...internal, + }); + } + }); -{ - // No type annotation of result type means `undefined` can be responded with - defineAiTool()({ - parameters: { type: "object" }, - render: ({ respond }) => { - expectType<(payload?: ToolResultResponse) => void>(respond); - respond(); - return null; - }, + test("should preserve locally-inferred types when assigned into AiOpaqueToolDefinition[]", () => { + // This tests that tool definitions will get locally inferred, not overridden + // by the array's *opaque* type they are getting assigned into! + const tools: AiOpaqueToolDefinition[] = [ + defineAiTool()({ + description: "First tool (execute, no render)", + parameters: { + type: "object", + properties: { foo: { type: "number" } }, + additionalProperties: false, + }, + execute: (args) => { + expectTypeOf(args).toEqualTypeOf<{ foo?: number }>(); + return { data: { ok: true } }; + }, + }), + + defineAiTool()({ + description: "Second tool (execute, no render)", + parameters: { + type: "object", + properties: { bar: { type: "string" } }, + additionalProperties: false, + }, + execute: (args) => { + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + return { data: { ok: true } }; + }, + }), + + defineAiTool()({ + description: "Third tool (execute & render)", + parameters: { + type: "object", + properties: { bar: { type: "string" } }, + additionalProperties: false, + }, + execute: (args) => { + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + return { data: { ok: true } }; + }, + render: ({ stage, partialArgs, args, result, respond }) => { + expectTypeOf(respond).toEqualTypeOf< + (payload?: ToolResultResponse) => void + >(); + + expectTypeOf(stage).toEqualTypeOf< + "receiving" | "executing" | "executed" + >(); + if (stage === "receiving") { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf(); + expectTypeOf(result).toEqualTypeOf(); + } else if (stage === "executing") { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + expectTypeOf(result).toEqualTypeOf(); + } else { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + expectTypeOf(result).toEqualTypeOf(); + } + return null; + }, + }), + + defineAiTool()({ + description: "Fourth tool (render, no execute)", + parameters: { + type: "object", + properties: { bar: { type: "string" } }, + additionalProperties: false, + }, + render: ({ stage, partialArgs, args, result, respond }) => { + expectTypeOf(respond).toEqualTypeOf< + (payload?: ToolResultResponse) => void + >(); + + expectTypeOf(stage).toEqualTypeOf< + "receiving" | "executing" | "executed" + >(); + if (stage === "receiving") { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf(); + expectTypeOf(result).toEqualTypeOf(); + } else if (stage === "executing") { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + expectTypeOf(result).toEqualTypeOf(); + } else { + expectTypeOf(partialArgs).toEqualTypeOf(); + expectTypeOf(args).toEqualTypeOf<{ bar?: string }>(); + expectTypeOf(result).toEqualTypeOf(); + } + return null; + }, + }), + ]; }); - // Same for execute! - defineAiTool()({ - parameters: { type: "object" }, - execute: () => {}, + test("should allow returning undefined when no explicit return type is given", () => { + defineAiTool()({ + parameters: { type: "object" }, + render: ({ respond }) => { + expectTypeOf(respond).toEqualTypeOf< + (payload?: ToolResultResponse) => void + >(); + respond(); + return null; + }, + }); + + // Same for execute! + defineAiTool()({ + parameters: { type: "object" }, + execute: () => {}, + }); }); -} -// But the same is no longer true if there is an explicit return type! -{ - expectError( + test("should enforce non-void return when an explicit return type is given", () => { defineAiTool<{ foo: string }>()({ parameters: { type: "object" }, - execute: () => { - /* cannot be empty, _must_ return `{ foo: string }` */ + // @ts-expect-error - `execute` cannot be empty, it _must_ return `{ foo: string }` + execute: () => {}, + }); + + defineAiTool<{ foo: string }>()({ + parameters: { type: "object" }, + render: ({ respond }) => { + expectTypeOf(respond).toEqualTypeOf< + (payload: ToolResultResponse<{ foo: string }>) => void + >(); + // @ts-expect-error - missing `{ foo: string }` in `respond()`, so an error + respond(); + return null; }, - }) - ); - - defineAiTool<{ foo: string }>()({ - parameters: { type: "object" }, - render: ({ respond }) => { - expectType<(payload: ToolResultResponse<{ foo: string }>) => void>( - respond - ); - expectError(respond(/* missing { foo: string }, so an error */)); - return null; - }, + }); }); -} -// However, it's fine again if all fields are optional...! -{ - defineAiTool<{ foo?: string }>()({ - parameters: { type: "object" }, - execute: () => { - /* it's fine to return nothing */ - }, - }); + test("should allow returning undefined when all fields in the explicit return type are optional", () => { + defineAiTool<{ foo?: string }>()({ + parameters: { type: "object" }, + execute: () => { + /* it's fine to return nothing */ + }, + }); - defineAiTool<{ foo?: string }>()({ - parameters: { type: "object" }, - render: ({ respond }) => { - expectType<(payload?: ToolResultResponse<{ foo?: string }>) => void>( - respond - ); - respond(); - return null; - }, + defineAiTool<{ foo?: string }>()({ + parameters: { type: "object" }, + render: ({ respond }) => { + expectTypeOf(respond).toEqualTypeOf< + (payload?: ToolResultResponse<{ foo?: string }>) => void + >(); + respond(); + return null; + }, + }); }); -} +}); diff --git a/packages/liveblocks-core/test-d/LiveObject-setLocal.test-d.ts b/packages/liveblocks-core/test-d/LiveObject-setLocal.test-d.ts index 59af5b03750..b8f5634814a 100644 --- a/packages/liveblocks-core/test-d/LiveObject-setLocal.test-d.ts +++ b/packages/liveblocks-core/test-d/LiveObject-setLocal.test-d.ts @@ -1,7 +1,6 @@ import { LiveList, LiveMap, LiveObject } from "@liveblocks/core"; -import { expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; -// Schema with various key types type Schema = { required: string; optionalJson?: number; @@ -16,49 +15,62 @@ type Schema = { declare const obj: LiveObject; -// Allowed: optional Json keys -obj.setLocal("optionalJson", 42); -obj.setLocal("optionalString", "hello"); -obj.setLocal("optionalJsonOrUndefined", "hello"); -obj.setLocal("optionalArray", [1, 2, 3]); -obj.setLocal("optionalObject", { nested: "hi", count: 5 }); +type IndexedSchema = { + [key: string]: string | number | undefined; + required: string; + localOnly?: string; +}; -// Disallowed: required key (not optional) -expectError(obj.setLocal("required", "value")); +declare const indexed: LiveObject; -// Disallowed: optional LiveStructure keys -expectError(obj.setLocal("optionalLiveObject", new LiveObject({ x: 1 }))); -expectError(obj.setLocal("optionalLiveList", new LiveList([1]))); -expectError(obj.setLocal("optionalLiveMap", new LiveMap([["a", "b"]]))); +describe("LiveObject.setLocal", () => { + test("should accept optional JSON keys", () => { + obj.setLocal("optionalJson", 42); + obj.setLocal("optionalString", "hello"); + obj.setLocal("optionalJsonOrUndefined", "hello"); + obj.setLocal("optionalArray", [1, 2, 3]); + obj.setLocal("optionalObject", { nested: "hi", count: 5 }); + }); -// Disallowed: wrong value type -expectError(obj.setLocal("optionalJson", "not a number")); + test("should reject required keys", () => { + // @ts-expect-error - Only optional keys can be set as local values + obj.setLocal("required", "value"); + }); -// Disallowed: undefined as value -expectError(obj.setLocal("optionalJson", undefined)); + test("should reject Live structures", () => { + // @ts-expect-error - Only plain JSON values can be set as local values + obj.setLocal("optionalLiveObject", new LiveObject({ x: 1 })); -// Disallowed: nonexistent key -expectError(obj.setLocal("nonexistent", 42)); + // @ts-expect-error - Only plain JSON values can be set as local values + obj.setLocal("optionalLiveList", new LiveList([1])); -// Return type of get() includes local values -expectType(obj.get("optionalJson")); -expectType(obj.get("required")); + // @ts-expect-error - Only plain JSON values can be set as local values + obj.setLocal("optionalLiveMap", new LiveMap([["a", "b"]])); + }); -// Index signature with optional values -type IndexedSchema = { - [key: string]: string | number | undefined; - required: string; - localOnly?: string; -}; + test("should reject wrong value types and nonexistent keys", () => { + // @ts-expect-error - `"optionalJson"` expects `number`, not `string` + obj.setLocal("optionalJson", "not a number"); -declare const indexed: LiveObject; + // @ts-expect-error - `undefined` is not allowed + obj.setLocal("optionalJson", undefined); + + // @ts-expect-error - `"nonexistent"` is not a key in the schema + obj.setLocal("nonexistent", 42); + }); + + test("should only allow explicitly-optional named keys on indexed schemas", () => { + indexed.setLocal("localOnly", "hello"); -// Allowed: the named optional Json key -indexed.setLocal("localOnly", "hello"); + // @ts-expect-error - `"required"` is not optional even though the index signature allows `string` values + indexed.setLocal("required", "hello"); -// Disallowed: we know that undefined isn't a legal value for 'required' field here, so don't allow it -expectError(indexed.setLocal("required", "hello")); -expectError(indexed.setLocal("unknownKey", "hello")); // Despite the index signature, we don't allow _any_ string key here. We require users to use explicitly-optional keys only. + // @ts-expect-error - Unknown keys are rejected even when the index signature would otherwise allow them, only explicitly-optional named keys are accepted + indexed.setLocal("nonexistent", "hello"); + }); -// Disallowed: wrong value type for localOnly -expectError(indexed.setLocal("localOnly", 42)); + test("should include local values in get() return type", () => { + expectTypeOf(obj.get("optionalJson")).toEqualTypeOf(); + expectTypeOf(obj.get("required")).toEqualTypeOf(); + }); +}); diff --git a/packages/liveblocks-core/test-d/Relax.test-d.ts b/packages/liveblocks-core/test-d/Relax.test-d.ts index 64e2384980c..b01dcc170ba 100644 --- a/packages/liveblocks-core/test-d/Relax.test-d.ts +++ b/packages/liveblocks-core/test-d/Relax.test-d.ts @@ -1,69 +1,67 @@ import type { Relax } from "@liveblocks/core"; -import { expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; -{ - type Actual = Relax<{ foo: string | number }>; - type Expected = { foo: string | number }; +describe("Relax", () => { + test("should be a no-op on a single-member union", () => { + type Actual = Relax<{ foo: string | number }>; + type Expected = { foo: string | number }; + let ru!: Actual; + expectTypeOf(ru).toEqualTypeOf(); + }); - let ru!: Actual; - expectType(ru); -} + test("should be a no-op when all union members share the same keys", () => { + type Actual = Relax<{ foo: string } | { foo: number }>; + type Expected = { foo: string } | { foo: number }; + let ru!: Actual; + expectTypeOf(ru).toEqualTypeOf(); + }); -{ - type Actual = Relax<{ foo: string } | { foo: number }>; - type Expected = { foo: string } | { foo: number }; + test("should add optional never for absent keys in disjoint unions", () => { + type Actual = Relax<{ foo: string } | { bar: number }>; + type Expected = { foo: string; bar?: never } | { bar: number; foo?: never }; + let ru!: Actual; + expectTypeOf(ru).toEqualTypeOf(); + }); - let ru!: Actual; - expectType(ru); -} + test("should add optional never for each absent key in unions", () => { + type Actual = Relax< + | { a: "hey"; b: string } + | { c: number; d: boolean } + | { e: Error; f: Date } + | { a: "hi"; d: boolean } + >; + type Expected = + | { a: "hey"; b: string; c?: never; d?: never; e?: never; f?: never } + | { c: number; d: boolean; a?: never; b?: never; e?: never; f?: never } + | { e: Error; f: Date; a?: never; b?: never; c?: never; d?: never } + | { a: "hi"; d: boolean; b?: never; c?: never; e?: never; f?: never }; + let ru!: Actual; + expectTypeOf(ru).toEqualTypeOf(); + }); -{ - type Actual = Relax<{ foo: string } | { bar: number }>; - type Expected = { foo: string; bar?: never } | { bar: number; foo?: never }; + test("should narrow down discriminated unions", () => { + type Actual = Relax< + | { type: "thing1"; payload: string } + | { type: "thing2"; payload: boolean } + | { type: "thing3"; payload: number } + | { type: "thing4" } + >; + let ru!: Actual; + expectTypeOf(ru.payload).toEqualTypeOf< + string | number | boolean | undefined + >(); + if (ru.type === "thing3") { + expectTypeOf(ru.payload).toEqualTypeOf(); + } + if (ru.payload === "string") { + expectTypeOf(ru.type).toEqualTypeOf<"thing1">(); + } + }); - let ru!: Actual; - expectType(ru); -} - -{ - type Actual = Relax< - | { a: "hey"; b: string } - | { c: number; d: boolean } - | { e: Error; f: Date } - | { a: "hi"; d: boolean } - >; - type Expected = - | { a: "hey"; b: string; c?: never; d?: never; e?: never; f?: never } - | { c: number; d: boolean; a?: never; b?: never; e?: never; f?: never } - | { e: Error; f: Date; a?: never; b?: never; c?: never; d?: never } - | { a: "hi"; d: boolean; b?: never; c?: never; e?: never; f?: never }; - - let ru!: Actual; - expectType(ru); -} - -{ - type Actual = Relax< - | { type: "thing1"; payload: string } - | { type: "thing2"; payload: boolean } - | { type: "thing3"; payload: number } - | { type: "thing4" } - >; - - let ru!: Actual; - expectType(ru.payload); - if (ru.type === "thing3") { - expectType(ru.payload); - } - if (ru.payload === "string") { - expectType<"thing1">(ru.type); - } -} - -{ - type Actual = Relax<{ a: string } | {}>; - type Expected = { a: string } | { a?: never }; - - let ru!: Actual; - expectType(ru); -} + test("should add optional never when a union member is empty", () => { + type Actual = Relax<{ a: string } | {}>; + type Expected = { a: string } | { a?: never }; + let ru!: Actual; + expectTypeOf(ru).toEqualTypeOf(); + }); +}); diff --git a/packages/liveblocks-core/test-d/ToolResultResponse.test-d.ts b/packages/liveblocks-core/test-d/ToolResultResponse.test-d.ts index 97f13837488..8dc1594f27f 100644 --- a/packages/liveblocks-core/test-d/ToolResultResponse.test-d.ts +++ b/packages/liveblocks-core/test-d/ToolResultResponse.test-d.ts @@ -1,68 +1,88 @@ -/* eslint-disable */ - import type { ToolResultResponse } from "@liveblocks/core"; -import { expectAssignable, expectNotAssignable } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; -// ❌ Invalid return values for execute() -{ - expectNotAssignable(123); - expectNotAssignable([1, 2, 3]); - expectNotAssignable({ yo: [1, 2, 3] }); +describe("ToolResultResponse", () => { + test("should reject invalid return values", () => { + // @ts-expect-error - Responses must be an object + ({}) satisfies ToolResultResponse; + // @ts-expect-error - Responses must be an object + [1, 2, 3] satisfies ToolResultResponse; - expectNotAssignable({ data: "hi" }); - expectNotAssignable({ data: 123 }); - expectNotAssignable({ data: [1, 2, 3] }); + // @ts-expect-error - Success responses must have a `data` field + ({ yo: [1, 2, 3] }) satisfies ToolResultResponse; - expectNotAssignable({ error: 123 }); - expectNotAssignable({ - error: { code: 403, message: "Not authorized" }, - }); + // @ts-expect-error - `data` must be an object as well + ({ data: "hi" }) satisfies ToolResultResponse; + // @ts-expect-error - `data` must be an object as well + ({ data: 123 }) satisfies ToolResultResponse; + // @ts-expect-error - `data` must be an object as well + ({ data: [1, 2, 3] }) satisfies ToolResultResponse; - expectNotAssignable({ cancel: 1 }); - expectNotAssignable({ cancel: false }); - expectNotAssignable({ cancel: null }); - expectNotAssignable({ cancel: [] }); - expectNotAssignable({ cancel: {} }); -} + // @ts-expect-error - `error` must be a string + ({ error: 123 }) satisfies ToolResultResponse; + ({ + // @ts-expect-error - `error` must be a string + error: { code: 403, message: "Not authorized" }, + }) satisfies ToolResultResponse; -// ❌ For now, don't allow description on error/cancelled results. -// It's a bit confusing. -// Besides, adding it back in later is still possible, but removing it -// is not, so this is the least risky option to launch with initially. -{ - expectNotAssignable({ - error: "oops", - description: "something went wrong", - }); - expectNotAssignable({ - cancel: true, - description: "cancelled by user", + // @ts-expect-error - `cancel` must be a boolean or a string (reason) + ({ cancel: 1 }) satisfies ToolResultResponse; + // @ts-expect-error - `cancel` must be a boolean or a string (reason) + ({ cancel: false }) satisfies ToolResultResponse; + // @ts-expect-error - `cancel` must be a boolean or a string (reason) + ({ cancel: null }) satisfies ToolResultResponse; + // @ts-expect-error - `cancel` must be a boolean or a string (reason) + ({ cancel: [] }) satisfies ToolResultResponse; + // @ts-expect-error - `cancel` must be a boolean or a string (reason) + ({ cancel: {} }) satisfies ToolResultResponse; }); - expectNotAssignable({ - cancel: "I want to cancel the operation", - description: "cancelled by user", - }); -} -// ✅ Valid return values for execute() -{ - // All of these are interpreted as type: "success" cases - expectAssignable({ data: {} }); - expectAssignable({ data: { yo: [1, 2, 3] } }); - expectAssignable({ data: {}, description: "all good" }); - expectAssignable({ - data: { yo: [1, 2, 3] }, - description: "all good", + // For now, don't allow description on error/cancelled results. + // It's a bit confusing. + // Besides, adding it back in later is still possible, but removing it + // is not, so this is the least risky option to launch with initially. + test("should disallow description on error/cancelled results", () => { + ({ + error: "oops", + description: "something went wrong", + // @ts-expect-error - `description` is not allowed on error/cancelled results + }) satisfies ToolResultResponse; + ({ + cancel: true, + description: "cancelled by user", + // @ts-expect-error - `description` is not allowed on error/cancelled results + }) satisfies ToolResultResponse; + ({ + cancel: "I want to cancel the operation", + description: "cancelled by user", + // @ts-expect-error - `description` is not allowed on error/cancelled results + }) satisfies ToolResultResponse; }); -} - -{ - expectAssignable({ error: "oops" }); -} -{ - expectAssignable({ cancel: true }); - expectAssignable({ - cancel: "I want to cancel the operation", + test("should accept valid shapes", () => { + // All of these are interpreted as type: "success" cases + expectTypeOf({ + data: {}, + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + data: { yo: [1, 2, 3] }, + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + data: {}, + description: "all good", + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + data: { yo: [1, 2, 3] }, + description: "all good", + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + error: "oops", + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + cancel: true, + } satisfies ToolResultResponse).toExtend(); + expectTypeOf({ + cancel: "I want to cancel the operation", + } satisfies ToolResultResponse).toExtend(); }); -} +}); diff --git a/packages/liveblocks-core/test-d/polyfills.test-d.ts b/packages/liveblocks-core/test-d/polyfills.test-d.ts index 32b3642fd01..d3fdc04a266 100644 --- a/packages/liveblocks-core/test-d/polyfills.test-d.ts +++ b/packages/liveblocks-core/test-d/polyfills.test-d.ts @@ -1,27 +1,27 @@ import NodeJSWebSocket from "ws"; import type { IWebSocket } from "@liveblocks/core"; -import { expectAssignable } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; // // NOTE: If one of these tests ever fails, it means our IWebSocket type (which // is our minimal subset between browser-based and NodeJS-based WebSocket // implementations) is incompatible with either one of the implementations. // -// This will most likely be reported as: +// This will be reported as: // -// Argument of type typeof WebSocket is not assignable to parameter of type IWebSocket. -// Types of parameters address and address are incompatible. -// Type string is not assignable to type null. -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// Type 'IWebSocket' does not satisfy the constraint '{ prototype: { ... }; ... }' // // Which is... not a very helpful message! 🤔 // -// The *actual* incompatibility probably lies much deeper in one of the other -// interface types. +// The incompatibility lies much deeper in one of the other interface types. // -// Browser-based websocket implementation is assignable to IWebSocket -expectAssignable(window.WebSocket); +describe("IWebSocket", () => { + test("should be assignable to a browser-based WebSocket implementation", () => { + expectTypeOf(window.WebSocket).toExtend(); + }); -// Node-based websocket implementation is assignable to IWebSocket -expectAssignable(NodeJSWebSocket); + test("should be assignable to a Node.js-based WebSocket implementation", () => { + expectTypeOf(NodeJSWebSocket).toExtend(); + }); +}); diff --git a/packages/liveblocks-core/vitest.config.typecheck.ts b/packages/liveblocks-core/vitest.config.typecheck.ts new file mode 100644 index 00000000000..8bdcb63a0e0 --- /dev/null +++ b/packages/liveblocks-core/vitest.config.typecheck.ts @@ -0,0 +1,6 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: makeTypecheckTestConfig(import.meta, ["test-d/**/*.test-d.ts"]), +}); diff --git a/packages/liveblocks-emails/package.json b/packages/liveblocks-emails/package.json index eae5d7c3163..2f8c53bca97 100644 --- a/packages/liveblocks-emails/package.json +++ b/packages/liveblocks-emails/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/emails", - "version": "3.18.0", + "version": "3.18.1", "description": "A set of functions and utilities to make sending emails based on Liveblocks notification events easy. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -33,12 +33,11 @@ "lint:package": "publint --strict && attw --pack", "test": "NODE_OPTIONS=\"--no-deprecation\" vitest run", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" vitest run", - "test:types": "ls test-d/* | xargs -n1 tsd --files", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0" + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc" diff --git a/packages/liveblocks-emails/test-d/index.test-d.ts b/packages/liveblocks-emails/test-d/index.test-d.ts deleted file mode 100644 index 54498b8e49a..00000000000 --- a/packages/liveblocks-emails/test-d/index.test-d.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO - Write test d diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json index a18cf1b8667..15412f60311 100644 --- a/packages/liveblocks-node-lexical/package.json +++ b/packages/liveblocks-node-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node-lexical", - "version": "3.18.0", + "version": "3.18.1", "description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -32,12 +32,11 @@ "lint:package": "publint --strict && attw --pack", "test": "NODE_OPTIONS=\"--no-deprecation\" vitest run", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" vitest run", - "test:types": "tsd", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0", + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1", "yjs": "^13.6.18" }, "peerDependencies": { diff --git a/packages/liveblocks-node-lexical/test-d/index.test-d.ts b/packages/liveblocks-node-lexical/test-d/index.test-d.ts deleted file mode 100644 index fd5d2a5d87c..00000000000 --- a/packages/liveblocks-node-lexical/test-d/index.test-d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// import * as lbnl from "@liveblocks/node-lexical"; -// import type { LexicalEditor } from "lexical"; -// import { withLexicalDocument } from "@liveblocks/node-lexical"; -// import { expectType } from "tsd"; -// import { Liveblocks } from "@liveblocks/node"; - -// const client = new Liveblocks({ secret: "sk_xxx" }); - -(async function main() { - // TODO: Test withLexicalDocument API - // withLexicalDocument -})(); diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json index 50bc795af4e..e22d3739adb 100644 --- a/packages/liveblocks-node-prosemirror/package.json +++ b/packages/liveblocks-node-prosemirror/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node-prosemirror", - "version": "3.18.0", + "version": "3.18.1", "description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -32,12 +32,11 @@ "lint:package": "publint --strict && attw --pack", "test": "NODE_OPTIONS=\"--no-deprecation\" vitest run", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" vitest run", - "test:types": "tsd -f test-d/index.test-d.ts", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0", - "@liveblocks/node": "3.18.0", + "@liveblocks/core": "3.18.1", + "@liveblocks/node": "3.18.1", "yjs": "^13.6.20" }, "peerDependencies": { diff --git a/packages/liveblocks-node-prosemirror/test-d/index.test-d.ts b/packages/liveblocks-node-prosemirror/test-d/index.test-d.ts deleted file mode 100644 index 5d4e43d9271..00000000000 --- a/packages/liveblocks-node-prosemirror/test-d/index.test-d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// import { expectType } from "tsd"; -// import { Liveblocks } from "@liveblocks/node"; - -// const client = new Liveblocks({ secret: "sk_xxx" }); - -(async function main() { - // TODO: Test withProsemirror API - // withProsemirror -})(); diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json index d8e074c6543..2204669749b 100644 --- a/packages/liveblocks-node/package.json +++ b/packages/liveblocks-node/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node", - "version": "3.18.0", + "version": "3.18.1", "description": "A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -32,11 +32,11 @@ "lint:package": "publint --strict && attw --pack", "test": "NODE_OPTIONS=\"--no-deprecation\" vitest run", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" vitest run", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/core": "3.18.0", + "@liveblocks/core": "3.18.1", "@stablelib/base64": "^1.0.1", "fast-sha256": "^1.3.0", "node-fetch": "^2.6.1" diff --git a/packages/liveblocks-node/test-d/augmentation.test-d.ts b/packages/liveblocks-node/test-d/augmentation.test-d.ts index 651dce1e4d7..bba27f920ff 100644 --- a/packages/liveblocks-node/test-d/augmentation.test-d.ts +++ b/packages/liveblocks-node/test-d/augmentation.test-d.ts @@ -1,13 +1,11 @@ -import { expectError, expectType } from "tsd"; import { Liveblocks } from "@liveblocks/node"; +import { describe, expectTypeOf, test } from "vitest"; +import { LiveList, LiveMap, LiveObject } from "@liveblocks/core"; import type { CommentReaction, CommentBody, CommentBodyBlockElement, CommentData, - LiveList, - LiveMap, - LiveObject, PlainLson, } from "@liveblocks/core"; @@ -61,190 +59,203 @@ declare global { } } -async () => { +describe("Liveblocks client with Liveblocks augmentation", () => { const client = new Liveblocks({ secret: "sk_xxx" }); - // .prepareSession() without all mandatory userInfo is an error - { - expectError(await client.prepareSession("user-123")); - expectError(await client.prepareSession("user-123", {})); - expectError(await client.prepareSession("user-123", { userInfo: {} })); - expectError( - await client.prepareSession("user-123", { userInfo: { name: "Vincent" } }) - ); - expectError( - await client.prepareSession("user-123", { userInfo: { age: 42 } }) - ); - } + test("should reject prepareSession() without mandatory userInfo", () => { + // @ts-expect-error - userInfo must include name and age + client.prepareSession("user-123"); + // @ts-expect-error - userInfo must include name and age + client.prepareSession("user-123", {}); + // @ts-expect-error - userInfo must include name and age + client.prepareSession("user-123", { userInfo: {} }); + client.prepareSession("user-123", { + // @ts-expect-error - userInfo must include age + userInfo: { name: "Vincent" }, + }); + client.prepareSession("user-123", { + // @ts-expect-error - userInfo must include name + userInfo: { age: 42 }, + }); + }); - // .prepareSession() with user info - { - const session = await client.prepareSession("user-123", { + test("should return a session with authorize() response types when correct userInfo is provided", async () => { + const session = client.prepareSession("user-123", { userInfo: { name: "Vincent", age: 42 }, }); session.allow("org1:*", session.READ_ACCESS); const resp = await session.authorize(); - expectType(resp.status); - expectType(resp.body); - expectType(resp.error); - } - - // .prepareSession() with incorrect user info - { - expectError( - await client.prepareSession("user-123", { userInfo: { foo: "bar" } }) - ); - } - - // .identifyUser() without all mandatory userInfo is an error - { - expectError(await client.identifyUser("user-123")); - expectError(await client.identifyUser("user-123", {})); - expectError(await client.identifyUser("user-123", { userInfo: {} })); - expectError( - await client.identifyUser("user-123", { userInfo: { name: "Vincent" } }) - ); - expectError( - await client.identifyUser("user-123", { userInfo: { age: 42 } }) - ); - } + expectTypeOf(resp.status).toEqualTypeOf(); + expectTypeOf(resp.body).toEqualTypeOf(); + expectTypeOf(resp.error).toEqualTypeOf(); + }); + + test("should reject prepareSession() with incorrect user info", () => { + client.prepareSession("user-123", { + userInfo: { + // @ts-expect-error - userInfo shape must match UserMeta + foo: "bar", + }, + }); + }); + + test("should reject identifyUser() without mandatory userInfo", async () => { + // @ts-expect-error - userInfo must include name and age + await client.identifyUser("user-123"); + // @ts-expect-error - userInfo must include name and age + await client.identifyUser("user-123", {}); + // @ts-expect-error - userInfo must include name and age + await client.identifyUser("user-123", { userInfo: {} }); + await client.identifyUser("user-123", { + // @ts-expect-error - userInfo must include age + userInfo: { name: "Vincent" }, + }); + await client.identifyUser("user-123", { + // @ts-expect-error - userInfo must include name + userInfo: { age: 42 }, + }); + }); - // .identifyUser() with user info - { + test("should return correct response types from identifyUser() when correct userInfo is provided", async () => { const resp = await client.identifyUser("user-123", { userInfo: { name: "Vincent", age: 42 }, }); - expectType(resp.status); - expectType(resp.body); - expectType(resp.error); - } + expectTypeOf(resp.status).toEqualTypeOf(); + expectTypeOf(resp.body).toEqualTypeOf(); + expectTypeOf(resp.error).toEqualTypeOf(); + }); + + test("should reject identifyUser() with incorrect user info", async () => { + await client.identifyUser("user-123", { + userInfo: { + // @ts-expect-error - userInfo shape must match UserMeta + foo: "bar", + }, + }); - // .identifyUser() with incorrect user info - { - expectError( - await client.identifyUser("user-123", { - userInfo: - // Not matching the annotations... - { foo: "bar" }, - }) - ); - - expectError( - await client.identifyUser("user-123", { - userInfo: - // ...or non-JSON - { notJson: new Date() }, - }) - ); - } + await client.identifyUser("user-123", { + userInfo: { + // @ts-expect-error - userInfo must be JSON-serializable + notJson: new Date(), + }, + }); + }); - // .getActiveUsers() - { + test("should return typed user info from getActiveUsers()", async () => { const users = (await client.getActiveUsers("my-room")).data; const user = users[0]!; - expectType<"user">(user.type); - expectType(user.connectionId); - expectType(user.id); - expectType(user.info.name); - expectType(user.info.age); - expectError(user.info.nonexisting); - } - - // .broadcastEvent() - { - expectError(client.broadcastEvent("my-room")); - expectError(client.broadcastEvent("my-room", { date: Date })); + expectTypeOf(user.type).toEqualTypeOf<"user">(); + expectTypeOf(user.connectionId).toEqualTypeOf(); + expectTypeOf(user.id).toEqualTypeOf(); + expectTypeOf(user.info.name).toEqualTypeOf(); + expectTypeOf(user.info.age).toEqualTypeOf(); + // @ts-expect-error - unknown UserMeta field + user.info.nonexisting; + }); + + test("should only accept typed RoomEvents in broadcastEvent()", async () => { + // @ts-expect-error - payload is required + client.broadcastEvent("my-room"); + client.broadcastEvent("my-room", { + // @ts-expect-error - invalid broadcast payload + date: Date, + }); - // Arbitrary JSON will be errors - expectError(client.broadcastEvent("my-room", 123)); - expectError(client.broadcastEvent("my-room", [1, 2, 3])); - expectError(client.broadcastEvent("my-room", { type: "foo" })); - expectError(client.broadcastEvent("my-room", { type: "boop" })); + // @ts-expect-error - event must match RoomEvent + client.broadcastEvent("my-room", 123); + // @ts-expect-error - event must match RoomEvent + client.broadcastEvent("my-room", [1, 2, 3]); + // @ts-expect-error - event must match RoomEvent + client.broadcastEvent("my-room", { type: "foo" }); + // @ts-expect-error - event must match RoomEvent + client.broadcastEvent("my-room", { type: "boop" }); - // Only correct room events can be sent await client.broadcastEvent("my-room", { type: "emoji", emoji: "😍" }); await client.broadcastEvent("my-room", { type: "beep" }); await client.broadcastEvent("my-room", { type: "beep", times: 3 }); - } + }); - // .getStorageDocument() (implicit plain LSON format) - { + test("should return PlainLson root from getStorageDocument()", async () => { const root = await client.getStorageDocument("my-room"); - expectType<"LiveObject">(root.liveblocksType); - expectType(root.data.liveblocksType); - } + expectTypeOf(root.liveblocksType).toEqualTypeOf<"LiveObject">(); + expectTypeOf(root.data.liveblocksType).toEqualTypeOf< + PlainLson | undefined + >(); + }); - // .getStorageDocument() (explicit plain LSON format) - { + test("should return PlainLson root from getStorageDocument() with 'plain-lson' format", async () => { const root = await client.getStorageDocument("my-room", "plain-lson"); - expectType<"LiveObject">(root.liveblocksType); - expectType(root.data.liveblocksType); - } + expectTypeOf(root.liveblocksType).toEqualTypeOf<"LiveObject">(); + expectTypeOf(root.data.liveblocksType).toEqualTypeOf< + PlainLson | undefined + >(); + }); - // .getStorageDocument() (simplified JSON format) - { + test("should return typed Storage shape from getStorageDocument() with 'json' format", async () => { const root = await client.getStorageDocument("my-room", "json"); - expectType(root.animals); - expectType(root.person.name); - expectType(root.person.age); - expectType(root.scores["foo"]); - } + expectTypeOf(root.animals).toEqualTypeOf(); + expectTypeOf(root.person.name).toEqualTypeOf(); + expectTypeOf(root.person.age).toEqualTypeOf(); + expectTypeOf(root.scores["foo"]).toEqualTypeOf(); + }); - // .getComment() - { + test("should return correct comment shape from getComment()", async () => { const comment = await client.getComment({ roomId: "my-room", threadId: "th_xxx", commentId: "cm_xxx", }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - expectType(comment.roomId); - expectType(comment.userId); - expectType(comment.createdAt); - expectType(comment.editedAt); - expectType(comment.reactions); - expectType(comment.deletedAt); - - expectType(comment.body); - expectType<1 | undefined>(comment.body?.version); - expectType(comment.body?.content); - } + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + expectTypeOf(comment.roomId).toEqualTypeOf(); + expectTypeOf(comment.userId).toEqualTypeOf(); + expectTypeOf(comment.createdAt).toEqualTypeOf(); + expectTypeOf(comment.editedAt).toEqualTypeOf(); + expectTypeOf(comment.reactions).toEqualTypeOf(); + expectTypeOf(comment.deletedAt).toEqualTypeOf(); + + expectTypeOf(comment.body).toEqualTypeOf(); + expectTypeOf(comment.body?.version).toEqualTypeOf<1 | undefined>(); + expectTypeOf(comment.body?.content).toEqualTypeOf< + CommentBodyBlockElement[] | undefined + >(); + }); + + test("should enforce typed metadata in createThread() and return typed thread", async () => { + // @ts-expect-error - invalid createThread arguments + client.createThread({ data: {} }); + // @ts-expect-error - invalid createThread arguments + client.createThread({ roomId: "my-room" }); + client.createThread({ + roomId: "my-room", + // @ts-expect-error - invalid createThread arguments + data: {}, + }); - // .createThread() - { - // Invalid calls - expectError(client.createThread({ data: {} })); - expectError(client.createThread({ roomId: "my-room" })); - expectError(client.createThread({ roomId: "my-room", data: {} })); - - // In an un-augmented world, this would be fine - expectError( - client.createThread({ - roomId: "my-room", - data: { - comment: { - userId: "user-123", - body: { version: 1, content: [] }, - }, + client.createThread({ + roomId: "my-room", + data: { + // @ts-expect-error - comment must include CommentMetadata + comment: { + userId: "user-123", + body: { version: 1, content: [] }, }, - }) - ); - - // In an un-augmented world, this would be fine - expectError( - client.createThread({ - roomId: "my-room", - data: { - comment: { - userId: "user-123", - body: { version: 1, content: [] }, - }, - metadata: { foo: "bar" }, // Arbitrary metadata! + }, + }); + + client.createThread({ + roomId: "my-room", + data: { + comment: { + userId: "user-123", + body: { version: 1, content: [] }, + metadata: { priority: 1 }, }, - }) - ); + // @ts-expect-error - ThreadMetadata shape + metadata: { foo: "bar" }, + }, + }); const thread = await client.createThread({ roomId: "room-123", @@ -258,11 +269,12 @@ async () => { }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType<"red" | "blue">(thread.metadata.color); - expectError(thread.metadata.nonexisting); - expectType(thread.comments); + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error - unknown ThreadMetadata field + thread.metadata.nonexisting; + expectTypeOf(thread.comments).toEqualTypeOf(); const threadWithCommentMetadata = await client.createThread({ roomId: "room-123", @@ -279,127 +291,137 @@ async () => { }, }); - expectType<"thread">(threadWithCommentMetadata.type); - expectType( + expectTypeOf(threadWithCommentMetadata.type).toEqualTypeOf<"thread">(); + expectTypeOf( threadWithCommentMetadata.comments[0]!.metadata.priority - ); - expectType( + ).toEqualTypeOf(); + expectTypeOf( threadWithCommentMetadata.comments[0]!.metadata.reviewed - ); - expectError(threadWithCommentMetadata.comments[0]!.metadata.nonexisting); - } + ).toEqualTypeOf(); + // @ts-expect-error - unknown CommentMetadata field + threadWithCommentMetadata.comments[0]!.metadata.nonexisting; + }); - // .editThreadMetadata() - { + test("should enforce typed metadata in editThreadMetadata()", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.editThreadMetadata({ roomId })); - expectError(client.editThreadMetadata({ threadId })); - expectError( - client.editThreadMetadata({ - roomId: "my-room", - threadId: "th_xxx", - data: {}, - }) - ); - expectError( - client.editThreadMetadata({ roomId, threadId, data: { userId } }) - ); - - // Incorrect metadata - expectError( - client.editThreadMetadata({ - roomId, - threadId, - data: { userId, metadata: { foo: "bar" } }, // Incorrect metadata - }) - ); + // @ts-expect-error - invalid editThreadMetadata arguments + client.editThreadMetadata({ roomId }); + // @ts-expect-error - invalid editThreadMetadata arguments + client.editThreadMetadata({ threadId }); + client.editThreadMetadata({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error - invalid editThreadMetadata arguments + data: {}, + }); + client.editThreadMetadata({ + roomId, + threadId, + // @ts-expect-error - invalid editThreadMetadata arguments + data: { userId }, + }); + + client.editThreadMetadata({ + roomId, + threadId, + data: { + userId, + // @ts-expect-error - metadata must match ThreadMetadata + metadata: { foo: "bar" }, + }, + }); await client.editThreadMetadata({ roomId, threadId, - data: { userId, metadata: {} }, // Not updating any fields is useless, but fine + data: { userId, metadata: {} }, }); await client.editThreadMetadata({ roomId, threadId, - data: { userId, metadata: { color: "red", pinned: null } }, // Correct metadata updates - }); - - expectError( - client.editThreadMetadata({ - roomId, - threadId, - data: { userId, metadata: { color: null } }, // Color cannot be set to null - }) - ); - - expectError( - client.editThreadMetadata({ - roomId, - threadId, - data: { userId, metadata: { foo: null } }, // Undefined fields - }) - ); - } + data: { userId, metadata: { color: "red", pinned: null } }, + }); + + client.editThreadMetadata({ + roomId, + threadId, + data: { + userId, + metadata: { + // @ts-expect-error - color cannot be null + color: null, + }, + }, + }); - // .getThreads() - { + client.editThreadMetadata({ + roomId, + threadId, + data: { + userId, + metadata: { + // @ts-expect-error - unknown ThreadMetadata field + foo: null, + }, + }, + }); + }); + + test("should return typed thread metadata from getThreads()", async () => { const threads = (await client.getThreads({ roomId: "my-room" })).data; const thread = threads[0]!; - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType<"red" | "blue">(thread.metadata.color); - expectError(thread.metadata.nonexisting); - expectType(thread.comments); - } - - // .getThread() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error - unknown ThreadMetadata field + thread.metadata.nonexisting; + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should return typed thread metadata from getThread()", async () => { const thread = await client.getThread({ roomId: "my-room", threadId: "th_xxx", }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType<"red" | "blue">(thread.metadata.color); - expectError(thread.metadata.nonexisting); - expectType(thread.comments); - } - - // .createComment() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error - unknown ThreadMetadata field + thread.metadata.nonexisting; + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should enforce typed metadata in createComment() and return typed comment", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.createComment({ roomId })); - expectError(client.createComment({ threadId })); - expectError( - client.createComment({ - roomId: "my-room", - threadId: "th_xxx", - data: {}, - }) - ); - expectError( - client.createComment({ - roomId: "my-room", - threadId: "th_xxx", - data: { userId }, - }) - ); + // @ts-expect-error - invalid createComment arguments + client.createComment({ roomId }); + // @ts-expect-error - invalid createComment arguments + client.createComment({ threadId }); + client.createComment({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error - invalid createComment arguments + data: {}, + }); + client.createComment({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error - invalid createComment arguments + data: { userId }, + }); const comment = await client.createComment({ roomId, @@ -411,9 +433,9 @@ async () => { }, }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); const commentWithMetadata = await client.createComment({ roomId, @@ -428,55 +450,58 @@ async () => { }, }); - expectType<"comment">(commentWithMetadata.type); - expectType(commentWithMetadata.metadata.priority); - expectType(commentWithMetadata.metadata.reviewed); - expectError(commentWithMetadata.metadata.nonexisting); - } + expectTypeOf(commentWithMetadata.type).toEqualTypeOf<"comment">(); + expectTypeOf(commentWithMetadata.metadata.priority).toEqualTypeOf(); + expectTypeOf(commentWithMetadata.metadata.reviewed).toEqualTypeOf< + boolean | undefined + >(); + // @ts-expect-error - unknown CommentMetadata field + commentWithMetadata.metadata.nonexisting; + }); - // .editCommentMetadata() - { + test("should enforce typed metadata in editCommentMetadata()", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const commentId = "cm_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.editCommentMetadata({ roomId })); - expectError(client.editCommentMetadata({ threadId })); - expectError(client.editCommentMetadata({ commentId })); - expectError( - client.editCommentMetadata({ - roomId: "my-room", - threadId: "th_xxx", - commentId: "cm_xxx", - data: {}, - }) - ); - expectError( - client.editCommentMetadata({ - roomId, - threadId, - commentId, - data: { userId }, - }) - ); - - // Incorrect metadata - expectError( - client.editCommentMetadata({ - roomId, - threadId, - commentId, - data: { userId, metadata: { foo: "bar" } }, // Incorrect metadata - }) - ); + // @ts-expect-error - invalid editCommentMetadata arguments + client.editCommentMetadata({ roomId }); + // @ts-expect-error - invalid editCommentMetadata arguments + client.editCommentMetadata({ threadId }); + // @ts-expect-error - invalid editCommentMetadata arguments + client.editCommentMetadata({ commentId }); + client.editCommentMetadata({ + roomId: "my-room", + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error - invalid editCommentMetadata arguments + data: {}, + }); + client.editCommentMetadata({ + roomId, + threadId, + commentId, + // @ts-expect-error - invalid editCommentMetadata arguments + data: { userId }, + }); + + client.editCommentMetadata({ + roomId, + threadId, + commentId, + data: { + userId, + // @ts-expect-error - metadata must match CommentMetadata + metadata: { foo: "bar" }, + }, + }); await client.editCommentMetadata({ roomId, threadId, commentId, - data: { userId, metadata: {} }, // Not updating any fields is useless, but fine + data: { userId, metadata: {} }, }); await client.editCommentMetadata({ @@ -486,30 +511,37 @@ async () => { data: { userId, metadata: { priority: 2, reviewed: null }, - }, // Correct metadata updates - }); - - expectError( - client.editCommentMetadata({ - roomId, - threadId, - commentId, - data: { userId, metadata: { priority: null } }, // priority cannot be set to null - }) - ); - - expectError( - client.editCommentMetadata({ - roomId, - threadId, - commentId, - data: { userId, metadata: { foo: null } }, // Undefined fields - }) - ); - } + }, + }); - // .addCommentReaction() - { + client.editCommentMetadata({ + roomId, + threadId, + commentId, + data: { + userId, + metadata: { + // @ts-expect-error - priority cannot be null + priority: null, + }, + }, + }); + + client.editCommentMetadata({ + roomId, + threadId, + commentId, + data: { + userId, + metadata: { + // @ts-expect-error - unknown CommentMetadata field + foo: null, + }, + }, + }); + }); + + test("should return correct reaction shape from addCommentReaction()", async () => { const reaction = await client.addCommentReaction({ roomId: "my-room", threadId: "th_xxx", @@ -520,13 +552,12 @@ async () => { }, }); - expectType(reaction.emoji); - expectType(reaction.userId); - expectType(reaction.createdAt); - } + expectTypeOf(reaction.emoji).toEqualTypeOf(); + expectTypeOf(reaction.userId).toEqualTypeOf(); + expectTypeOf(reaction.createdAt).toEqualTypeOf(); + }); - // .markThreadAsResolved() - { + test("should return correct thread shape from markThreadAsResolved()", async () => { const thread = await client.markThreadAsResolved({ roomId: "my-room", threadId: "th_xxx", @@ -534,17 +565,16 @@ async () => { userId: "user-id", }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.resolved); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.comments); - } - - // .markThreadAsUnresolved() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.resolved).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should return correct thread shape from markThreadAsUnresolved()", async () => { const thread = await client.markThreadAsUnresolved({ roomId: "my-room", threadId: "th_xxx", @@ -552,12 +582,12 @@ async () => { userId: "user-id", }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.resolved); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.comments); - } -}; + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.resolved).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); +}); diff --git a/packages/liveblocks-node/test-d/no-augmentation.test-d.ts b/packages/liveblocks-node/test-d/no-augmentation.test-d.ts index d736d56c83c..77447fd9714 100644 --- a/packages/liveblocks-node/test-d/no-augmentation.test-d.ts +++ b/packages/liveblocks-node/test-d/no-augmentation.test-d.ts @@ -1,5 +1,5 @@ -import { expectError, expectType } from "tsd"; import { Liveblocks } from "@liveblocks/node"; +import { describe, expectTypeOf, test } from "vitest"; import type { CommentReaction, CommentBody, @@ -10,98 +10,88 @@ import type { ReadonlyJsonObject, } from "@liveblocks/core"; -async () => { +describe("Liveblocks client without Liveblocks augmentation", () => { const client = new Liveblocks({ secret: "sk_xxx" }); - // .prepareSession() - { - const session = await client.prepareSession("user-123"); + test("should return a session with authorize() response types", async () => { + const session = client.prepareSession("user-123"); session.allow("org1:*", session.READ_ACCESS); const resp = await session.authorize(); - expectType(resp.status); - expectType(resp.body); - expectType(resp.error); - } - - // .prepareSession() with user info - { - const session = await client.prepareSession("user-123", { + expectTypeOf(resp.status).toEqualTypeOf(); + expectTypeOf(resp.body).toEqualTypeOf(); + expectTypeOf(resp.error).toEqualTypeOf(); + }); + + test("should return a session with authorize() response types when userInfo is provided", async () => { + const session = client.prepareSession("user-123", { userInfo: { name: "Vincent", age: 42 }, }); session.allow("org1:*", session.READ_ACCESS); const resp = await session.authorize(); - expectType(resp.status); - expectType(resp.body); - expectType(resp.error); - } - - // .prepareSession() with arbitrary user info - { - await client.prepareSession("user-123", { - userInfo: - // Arbitrary user info is fine... - { foo: "bar" }, + expectTypeOf(resp.status).toEqualTypeOf(); + expectTypeOf(resp.body).toEqualTypeOf(); + expectTypeOf(resp.error).toEqualTypeOf(); + }); + + test("should reject non-JSON values in prepareSession() userInfo", () => { + client.prepareSession("user-123", { + userInfo: { foo: "bar" }, }); - expectError( - await client.prepareSession("user-123", { - userInfo: - // ...but non-JSON is not - { foo: "bar", notJson: new Date() }, - }) - ); - } - - // .identifyUser() bare - { + client.prepareSession("user-123", { + userInfo: { + foo: "bar", + // @ts-expect-error non-JSON in userInfo + notJson: new Date(), + }, + }); + }); + + test("should accept identifyUser() without userInfo", async () => { await client.identifyUser("user-123"); - } + }); - // .identifyUser() with user info - { + test("should return correct response types from identifyUser()", async () => { const resp = await client.identifyUser("user-123", { userInfo: { name: "Vincent", age: 42 }, }); - expectType(resp.status); - expectType(resp.body); - expectType(resp.error); - } + expectTypeOf(resp.status).toEqualTypeOf(); + expectTypeOf(resp.body).toEqualTypeOf(); + expectTypeOf(resp.error).toEqualTypeOf(); + }); - // .identifyUser() with arbitrary user info - { + test("should reject non-JSON values in identifyUser() userInfo", async () => { await client.identifyUser("user-123", { - userInfo: - // Arbitrary user info is fine... - { foo: "bar" }, + userInfo: { foo: "bar" }, }); - expectError( - await client.identifyUser("user-123", { - userInfo: - // ...but non-JSON is not - { foo: "bar", notJson: new Date() }, - }) - ); - } - - // .getActiveUsers() - { + await client.identifyUser("user-123", { + userInfo: { + foo: "bar", + // @ts-expect-error non-JSON in userInfo + notJson: new Date(), + }, + }); + }); + + test("should return loosely typed user info from getActiveUsers()", async () => { const users = (await client.getActiveUsers("my-room")).data; const user = users[0]!; - expectType<"user">(user.type); - expectType(user.connectionId); - expectType(user.id); + expectTypeOf(user.type).toEqualTypeOf<"user">(); + expectTypeOf(user.connectionId).toEqualTypeOf(); + expectTypeOf(user.id).toEqualTypeOf(); const info = user.info!; - expectType(info.name); - expectType(info.age); - expectType(info.nonexisting); - } + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.age).toEqualTypeOf(); + expectTypeOf(info.nonexisting).toEqualTypeOf(); + }); - // .broadcastEvent() - { - expectError(client.broadcastEvent("my-room")); - expectError(client.broadcastEvent("my-room", { date: Date })); + test("should accept any JSON payload in broadcastEvent()", async () => { + // @ts-expect-error payload is required + client.broadcastEvent("my-room"); + // @ts-expect-error invalid broadcast payload + client.broadcastEvent("my-room", { date: Date }); await client.broadcastEvent("my-room", 123); await client.broadcastEvent("my-room", [1, 2, 3]); @@ -110,58 +100,63 @@ async () => { await client.broadcastEvent("my-room", { type: "emoji", emoji: "😍" }); await client.broadcastEvent("my-room", { type: "beep" }); await client.broadcastEvent("my-room", { type: "beep", times: 3 }); - } + }); - // .getStorageDocument() (implicit plain LSON format) - { + test("should return PlainLson root from getStorageDocument()", async () => { const root = await client.getStorageDocument("my-room"); - expectType<"LiveObject">(root.liveblocksType); - expectType(root.data.liveblocksType); - } + expectTypeOf(root.liveblocksType).toEqualTypeOf<"LiveObject">(); + expectTypeOf(root.data.liveblocksType).toEqualTypeOf< + PlainLson | undefined + >(); + }); - // .getStorageDocument() (explicit plain LSON format) - { + test("should return PlainLson root from getStorageDocument() with 'plain-lson' format", async () => { const root = await client.getStorageDocument("my-room", "plain-lson"); - expectType<"LiveObject">(root.liveblocksType); - expectType(root.data.liveblocksType); - } + expectTypeOf(root.liveblocksType).toEqualTypeOf<"LiveObject">(); + expectTypeOf(root.data.liveblocksType).toEqualTypeOf< + PlainLson | undefined + >(); + }); - // .getStorageDocument() (simplified JSON format) - { + test("should return ReadonlyJsonObject from getStorageDocument() with 'json' format", async () => { const root = await client.getStorageDocument("my-room", "json"); - expectType(root); - } + expectTypeOf(root).toEqualTypeOf(); + }); - // .getComment() - { + test("should return correct comment shape from getComment()", async () => { const comment = await client.getComment({ roomId: "my-room", threadId: "th_xxx", commentId: "cm_xxx", }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - expectType(comment.roomId); - expectType(comment.userId); - expectType(comment.createdAt); - expectType(comment.editedAt); - expectType(comment.reactions); - expectType(comment.deletedAt); - - expectType(comment.body); - expectType<1 | undefined>(comment.body?.version); - expectType(comment.body?.content); - } - - // .createThread() - { - // Invalid calls - expectError(client.createThread({ data: {} })); - expectError(client.createThread({ roomId: "my-room" })); - expectError(client.createThread({ roomId: "my-room", data: {} })); - - // In this un-augmented world, this is fine + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + expectTypeOf(comment.roomId).toEqualTypeOf(); + expectTypeOf(comment.userId).toEqualTypeOf(); + expectTypeOf(comment.createdAt).toEqualTypeOf(); + expectTypeOf(comment.editedAt).toEqualTypeOf(); + expectTypeOf(comment.reactions).toEqualTypeOf(); + expectTypeOf(comment.deletedAt).toEqualTypeOf(); + + expectTypeOf(comment.body).toEqualTypeOf(); + expectTypeOf(comment.body?.version).toEqualTypeOf<1 | undefined>(); + expectTypeOf(comment.body?.content).toEqualTypeOf< + CommentBodyBlockElement[] | undefined + >(); + }); + + test("should accept arbitrary metadata in createThread() and return loosely typed thread", async () => { + // @ts-expect-error invalid createThread arguments + client.createThread({ data: {} }); + // @ts-expect-error invalid createThread arguments + client.createThread({ roomId: "my-room" }); + client.createThread({ + roomId: "my-room", + // @ts-expect-error invalid createThread arguments + data: {}, + }); + client.createThread({ roomId: "my-room", data: { @@ -172,7 +167,6 @@ async () => { }, }); - // In this un-augmented world, this is fine client.createThread({ roomId: "my-room", data: { @@ -180,7 +174,7 @@ async () => { userId: "user-123", body: { version: 1, content: [] }, }, - metadata: { foo: "bar" }, // Arbitrary metadata! + metadata: { foo: "bar" }, }, }); @@ -195,36 +189,39 @@ async () => { }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.metadata.color); - expectType( - thread.metadata.nonexisting - ); - expectType(thread.comments); - } - - // .editThreadMetadata() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should accept arbitrary metadata in editThreadMetadata()", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.editThreadMetadata({ roomId })); - expectError(client.editThreadMetadata({ threadId })); - expectError( - client.editThreadMetadata({ - roomId: "my-room", - threadId: "th_xxx", - data: {}, - }) - ); - expectError( - client.editThreadMetadata({ roomId, threadId, data: { userId } }) - ); - - // Arbitrary metadata updates are fine in an unaugmented world + // @ts-expect-error invalid editThreadMetadata arguments + client.editThreadMetadata({ roomId }); + // @ts-expect-error invalid editThreadMetadata arguments + client.editThreadMetadata({ threadId }); + client.editThreadMetadata({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error invalid editThreadMetadata arguments + data: {}, + }); + client.editThreadMetadata({ + roomId, + threadId, + // @ts-expect-error invalid editThreadMetadata arguments + data: { userId }, + }); + await client.editThreadMetadata({ roomId, threadId, @@ -234,73 +231,73 @@ async () => { await client.editThreadMetadata({ roomId, threadId, - data: { userId, metadata: {} }, // Not updating any fields is useless, but fine + data: { userId, metadata: {} }, }); await client.editThreadMetadata({ roomId, threadId, - data: { userId, metadata: { color: "red", pinned: null } }, // Correct metadata updates + data: { userId, metadata: { color: "red", pinned: null } }, }); - } + }); - // .getThreads() - { + test("should return loosely typed thread metadata from getThreads()", async () => { const threads = (await client.getThreads({ roomId: "my-room" })).data; const thread = threads[0]!; - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.metadata.foo); - expectType( - thread.metadata.nonexisting - ); - expectType(thread.comments); - } - - // .getThread() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should return loosely typed thread metadata from getThread()", async () => { const thread = await client.getThread({ roomId: "my-room", threadId: "th_xxx", }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.metadata.foo); - expectType( - thread.metadata.nonexisting - ); - expectType(thread.comments); - } - - // .createComment() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should accept arbitrary metadata in createComment() and return loosely typed comment", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.createComment({ roomId })); - expectError(client.createComment({ threadId })); - expectError( - client.createComment({ - roomId: "my-room", - threadId: "th_xxx", - data: {}, - }) - ); - expectError( - client.createComment({ - roomId: "my-room", - threadId: "th_xxx", - data: { userId }, - }) - ); + // @ts-expect-error invalid createComment arguments + client.createComment({ roomId }); + // @ts-expect-error invalid createComment arguments + client.createComment({ threadId }); + client.createComment({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error invalid createComment arguments + data: {}, + }); + client.createComment({ + roomId: "my-room", + threadId: "th_xxx", + // @ts-expect-error invalid createComment arguments + data: { userId }, + }); const comment = await client.createComment({ roomId, @@ -311,10 +308,12 @@ async () => { }, }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - expectType(comment.metadata.foo); + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + expectTypeOf(comment.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); const commentWithMetadata = await client.createComment({ roomId, @@ -326,44 +325,42 @@ async () => { }, }); - expectType<"comment">(commentWithMetadata.type); - expectType( - commentWithMetadata.metadata.status - ); - expectType( - commentWithMetadata.metadata.priority - ); - } - - // .editCommentMetadata() - { + expectTypeOf(commentWithMetadata.type).toEqualTypeOf<"comment">(); + expectTypeOf(commentWithMetadata.metadata.status).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(commentWithMetadata.metadata.priority).toEqualTypeOf< + string | number | boolean | undefined + >(); + }); + + test("should accept arbitrary metadata in editCommentMetadata()", async () => { const roomId = "my-room"; const threadId = "th_xxx"; const commentId = "cm_xxx"; const userId = "user-123"; - // Invalid calls - expectError(client.editCommentMetadata({ roomId })); - expectError(client.editCommentMetadata({ threadId })); - expectError(client.editCommentMetadata({ commentId })); - expectError( - client.editCommentMetadata({ - roomId: "my-room", - threadId: "th_xxx", - commentId: "cm_xxx", - data: {}, - }) - ); - expectError( - client.editCommentMetadata({ - roomId, - threadId, - commentId, - data: { userId }, - }) - ); - - // Arbitrary metadata updates are fine in an unaugmented world + // @ts-expect-error invalid editCommentMetadata arguments + client.editCommentMetadata({ roomId }); + // @ts-expect-error invalid editCommentMetadata arguments + client.editCommentMetadata({ threadId }); + // @ts-expect-error invalid editCommentMetadata arguments + client.editCommentMetadata({ commentId }); + client.editCommentMetadata({ + roomId: "my-room", + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error invalid editCommentMetadata arguments + data: {}, + }); + client.editCommentMetadata({ + roomId, + threadId, + commentId, + // @ts-expect-error invalid editCommentMetadata arguments + data: { userId }, + }); + await client.editCommentMetadata({ roomId, threadId, @@ -375,7 +372,7 @@ async () => { roomId, threadId, commentId, - data: { userId, metadata: {} }, // Not updating any fields is useless, but fine + data: { userId, metadata: {} }, }); await client.editCommentMetadata({ @@ -384,10 +381,9 @@ async () => { commentId, data: { userId, metadata: { priority: 2, reviewed: null } }, }); - } + }); - // .addCommentReaction() - { + test("should return correct reaction shape from addCommentReaction()", async () => { const reaction = await client.addCommentReaction({ roomId: "my-room", threadId: "th_xxx", @@ -398,12 +394,12 @@ async () => { }, }); - expectType(reaction.emoji); - expectType(reaction.userId); - expectType(reaction.createdAt); - } - // .markThreadAsResolved() - { + expectTypeOf(reaction.emoji).toEqualTypeOf(); + expectTypeOf(reaction.userId).toEqualTypeOf(); + expectTypeOf(reaction.createdAt).toEqualTypeOf(); + }); + + test("should return correct thread shape from markThreadAsResolved()", async () => { const thread = await client.markThreadAsResolved({ roomId: "my-room", threadId: "th_xxx", @@ -411,16 +407,16 @@ async () => { userId: "user-id", }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.resolved); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.comments); - } - // .markThreadAsUnresolved() - { + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.resolved).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); + + test("should return correct thread shape from markThreadAsUnresolved()", async () => { const thread = await client.markThreadAsUnresolved({ roomId: "my-room", threadId: "th_xxx", @@ -428,12 +424,12 @@ async () => { userId: "user-id", }, }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType(thread.resolved); - expectType(thread.createdAt); - expectType(thread.updatedAt); - expectType(thread.comments); - } -}; + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.resolved).toEqualTypeOf(); + expectTypeOf(thread.createdAt).toEqualTypeOf(); + expectTypeOf(thread.updatedAt).toEqualTypeOf(); + expectTypeOf(thread.comments).toEqualTypeOf(); + }); +}); diff --git a/packages/liveblocks-node/vitest.config.typecheck.ts b/packages/liveblocks-node/vitest.config.typecheck.ts new file mode 100644 index 00000000000..8ae214912c0 --- /dev/null +++ b/packages/liveblocks-node/vitest.config.typecheck.ts @@ -0,0 +1,25 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +// Multiple configs are used when testing augmentation and global +// declerations, to avoid leaking globals between tests. +export default defineConfig({ + test: { + projects: [ + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/augmentation.test-d.ts"], + "augmentation" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/no-augmentation.test-d.ts"], + "no-augmentation" + ), + }, + ], + }, +}); diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json index 04c26e60135..4a21b27f867 100644 --- a/packages/liveblocks-react-blocknote/package.json +++ b/packages/liveblocks-react-blocknote/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-blocknote", - "version": "3.18.0", + "version": "3.18.1", "description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -44,12 +44,12 @@ "test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-tiptap": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-tiptap": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "@tiptap/core": "^3.19.0", "vitest-tsconfig-paths": "^3.4.1" }, diff --git a/packages/liveblocks-react-blocknote/test-d/index.test-d.ts b/packages/liveblocks-react-blocknote/test-d/index.test-d.ts deleted file mode 100644 index 4d6fe194844..00000000000 --- a/packages/liveblocks-react-blocknote/test-d/index.test-d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// import * as lbrl from "@liveblocks/react-tiptap"; -import { expectType } from "tsd"; - -(async function main() { - expectType("hi".toString()); - - // TODO: Write type-level tests for these exports - // expectType(lbrl.FloatingComposer); - // expectType(lbrl.FloatingThreads); - // expectType(lbrl.LiveblocksExtension); - // expectType(lbrl.AnchoredThreads); -})(); diff --git a/packages/liveblocks-react-flow/package.json b/packages/liveblocks-react-flow/package.json index 74ced719ff3..4d041f74a36 100644 --- a/packages/liveblocks-react-flow/package.json +++ b/packages/liveblocks-react-flow/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-flow", - "version": "3.18.0", + "version": "3.18.1", "description": "An integration of React Flow to enable collaboration and realtime cursors with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -59,14 +59,14 @@ "lint": "eslint src/ && stylelint src/styles/", "test": "npx liveblocks dev -p 1154 -c 'vitest run --coverage'", "test:ci": "vitest run", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1" }, "peerDependencies": { "@xyflow/react": "^12", diff --git a/packages/liveblocks-react-flow/test-d/mutate-flow.test-d.ts b/packages/liveblocks-react-flow/test-d/mutate-flow.test-d.ts index 3d35f5baa4d..a57481c4197 100644 --- a/packages/liveblocks-react-flow/test-d/mutate-flow.test-d.ts +++ b/packages/liveblocks-react-flow/test-d/mutate-flow.test-d.ts @@ -1,11 +1,7 @@ -/* eslint-disable */ - import type { Edge, Node } from "@xyflow/react"; -import { expectError, expectType } from "tsd"; - -import type { MutableFlow } from "../dist/node"; +import { describe, expectTypeOf, test } from "vitest"; -// -- Custom types used by the tests below -- +import type { MutableFlow } from "@liveblocks/react-flow/node"; type CustomNodeData = { label: string; priority: number }; type CustomNode = Node; @@ -13,133 +9,116 @@ type CustomNode = Node; type CustomEdgeData = { weight: number }; type CustomEdge = Edge; -/** - * MutableFlow with custom node/edge types — getters - */ -{ - const flow = {} as MutableFlow; - - expectType(flow.nodes); - expectType(flow.edges); - expectType<{ nodes: readonly CustomNode[]; edges: readonly CustomEdge[] }>( - flow.toJSON() - ); - expectType(flow.getNode("n1")); - expectType(flow.getEdge("e1")); -} - -/** - * MutableFlow — addNode requires correct shape - */ -{ - const flow = {} as MutableFlow; - - // Correct node should be accepted - flow.addNode({ - id: "n1", - type: "task", - position: { x: 0, y: 0 }, - data: { label: "Hello", priority: 1 }, +describe("MutableFlow", () => { + describe("getters", () => { + test("should expose typed nodes, edges, and accessors", () => { + const flow = {} as MutableFlow; + + expectTypeOf(flow.nodes).toEqualTypeOf(); + expectTypeOf(flow.edges).toEqualTypeOf(); + expectTypeOf(flow.toJSON()).toEqualTypeOf<{ + nodes: readonly CustomNode[]; + edges: readonly CustomEdge[]; + }>(); + expectTypeOf(flow.getNode("n1")).toEqualTypeOf(); + expectTypeOf(flow.getEdge("e1")).toEqualTypeOf(); + }); + }); + + describe("addNode", () => { + test("should require correct node shape", () => { + const flow = {} as MutableFlow; + + flow.addNode({ + id: "n1", + type: "task", + position: { x: 0, y: 0 }, + data: { label: "Hello", priority: 1 }, + }); + + flow.addNode({ + id: "n2", + type: "task", + position: { x: 0, y: 0 }, + // @ts-expect-error - Missing required `priority` on data + data: { label: "Hello" }, + }); + + flow.addNode({ + id: "n3", + // @ts-expect-error - Wrong node `type` literal + type: "wrong", + position: { x: 0, y: 0 }, + data: { label: "Hello", priority: 1 }, + }); + }); }); - // Missing required data field should error - expectError( - flow.addNode({ - id: "n2", - type: "task", - position: { x: 0, y: 0 }, - data: { label: "Hello" }, - }) - ); - - // Wrong node type should error - expectError( - flow.addNode({ - id: "n3", - type: "wrong", - position: { x: 0, y: 0 }, - data: { label: "Hello", priority: 1 }, - }) - ); -} - -/** - * MutableFlow — addEdge requires correct shape - */ -{ - const flow = {} as MutableFlow; - - // Correct edge should be accepted - flow.addEdge({ - id: "e1", - type: "weighted", - source: "n1", - target: "n2", - data: { weight: 5 }, + describe("addEdge", () => { + test("should require correct edge shape", () => { + const flow = {} as MutableFlow; + + flow.addEdge({ + id: "e1", + type: "weighted", + source: "n1", + target: "n2", + data: { weight: 5 }, + }); + + flow.addEdge({ + id: "e2", + // @ts-expect-error - Wrong edge `type` literal + type: "wrong", + source: "n1", + target: "n2", + data: { weight: 5 }, + }); + }); }); - // Wrong edge type should error - expectError( - flow.addEdge({ - id: "e2", - type: "wrong", - source: "n1", - target: "n2", - data: { weight: 5 }, - }) - ); -} - -/** - * MutableFlow — updateNode - */ -{ - const flow = {} as MutableFlow; - - // Partial update - flow.updateNode("n1", { position: { x: 10, y: 20 } }); - - // Updater function receives the correct type - flow.updateNode("n1", (node) => { - expectType(node); - return { ...node, position: { x: 0, y: 0 } }; + describe("updateNode", () => { + test("should accept partial updates and typed updaters", () => { + const flow = {} as MutableFlow; + + flow.updateNode("n1", { position: { x: 10, y: 20 } }); + + flow.updateNode("n1", (node) => { + expectTypeOf(node).toEqualTypeOf(); + return { ...node, position: { x: 0, y: 0 } }; + }); + }); }); -} -/** - * MutableFlow — updateNodeData - */ -{ - const flow = {} as MutableFlow; + describe("updateNodeData", () => { + test("should accept partial data and typed updaters", () => { + const flow = {} as MutableFlow; - // Partial data update with known key - flow.updateNodeData("n1", { priority: 2 }); + flow.updateNodeData("n1", { priority: 2 }); - // Unknown data key should error - expectError(flow.updateNodeData("n1", { unknown: true })); + // @ts-expect-error - Unknown data key + flow.updateNodeData("n1", { unknown: true }); - // Updater function receives the correct data type - flow.updateNodeData("n1", (data) => { - expectType(data); - return { ...data, priority: data.priority + 1 }; + flow.updateNodeData("n1", (data) => { + expectTypeOf(data).toEqualTypeOf(); + return { ...data, priority: data.priority + 1 }; + }); + }); }); -} -/** - * MutableFlow — updateEdgeData - */ -{ - const flow = {} as MutableFlow; + describe("updateEdgeData", () => { + test("should accept partial data and typed updaters", () => { + const flow = {} as MutableFlow; - // Partial data update with known key - flow.updateEdgeData("e1", { weight: 5 }); + flow.updateEdgeData("e1", { weight: 5 }); - // Unknown data key should error - expectError(flow.updateEdgeData("e1", { unknown: true })); + // @ts-expect-error - Unknown data key + flow.updateEdgeData("e1", { unknown: true }); - // Updater function receives possibly-undefined data (edge data is optional in React Flow) - flow.updateEdgeData("e1", (data) => { - expectType(data); - return { ...data!, weight: data!.weight + 1 }; + flow.updateEdgeData("e1", (data) => { + expectTypeOf(data).toEqualTypeOf(); + return { ...data!, weight: data!.weight + 1 }; + }); + }); }); -} +}); diff --git a/packages/liveblocks-react-flow/test-d/types.test-d.ts b/packages/liveblocks-react-flow/test-d/types.test-d.ts index 0496f60bd4a..35d15a208e1 100644 --- a/packages/liveblocks-react-flow/test-d/types.test-d.ts +++ b/packages/liveblocks-react-flow/test-d/types.test-d.ts @@ -1,5 +1,3 @@ -/* eslint-disable */ - import type { LiveList, LiveMap, @@ -7,7 +5,7 @@ import type { LsonObject, } from "@liveblocks/core"; import type { Edge, Node, XYPosition } from "@xyflow/react"; -import { expectAssignable, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; import type { LiveblocksEdge, @@ -33,154 +31,148 @@ type CustomNode = Node; type CustomEdge = Edge; -/** - * LiveblocksNode - */ -{ - const node = {} as LiveblocksNode; +describe("LiveblocksNode", () => { + test("should reflect LiveObject shape and base sync config", () => { + const node = {} as LiveblocksNode; - // The node itself becomes a LiveObject. - expectAssignable>(node); + expectTypeOf(node).toExtend>(); - // Its root fields are "liveified" or not according to the base sync config. - { - // Synced fields are "liveified": scalars stay unchanged, objects become LiveObjects, and arrays become LiveLists. - expectAssignable | undefined>(node.get("style")); - expectType(node.get("id")); + expectTypeOf(node.get("style")).toExtend< + LiveObject | undefined + >(); + expectTypeOf(node.get("id")).toEqualTypeOf(); - // `selected` is marked as local-only in the base sync config, so it becomes optional. - expectType(node.get("selected")); + expectTypeOf(node.get("selected")).toEqualTypeOf(); - // `position` is marked as atomic in the base sync config, so it stays as is instead of becoming a LiveObject. - expectType(node.get("position")); - } + expectTypeOf(node.get("position")).toEqualTypeOf(); + }); - // The `data` field follows the sync config provided as a second generic. - { - // If no custom sync config is passed, everything is synced. - { + describe("data field (default sync)", () => { + test("should liveify all data fields when no custom sync config is passed", () => { const node = {} as LiveblocksNode; - // The `data` field is always a LiveObject. - expectAssignable>(node.get("data")); - - // Synced fields are "liveified": scalars stay unchanged, objects become LiveObjects, and arrays become LiveLists. - expectType(node.get("data").get("string")); - expectAssignable>(node.get("data").get("array")); - expectAssignable>(node.get("data").get("nested")); - expectAssignable>( - node.get("data").get("nested").get("nested")! - ); - } - - // If a custom sync config is passed, it applies to the types as expected. - { + expectTypeOf(node.get("data")).toExtend>(); + + expectTypeOf(node.get("data").get("string")).toEqualTypeOf(); + expectTypeOf(node.get("data").get("array")).toExtend>(); + expectTypeOf(node.get("data").get("nested")).toExtend< + LiveObject + >(); + expectTypeOf(node.get("data").get("nested").get("nested")!).toExtend< + LiveObject + >(); + }); + }); + + describe("data field (custom sync config)", () => { + test("should apply atomic and local-only overrides from sync config", () => { const node = {} as LiveblocksNode< CustomNode, { object: "atomic"; array: false; nested: { nested: false } } >; - // The `data` field is always a LiveObject. - expectAssignable>(node.get("data")); - - // Fields not present in the custom sync config are still synced, so they are still "liveified". - expectType(node.get("data").get("string")); - expectAssignable>(node.get("data").get("nested")); - - // `object` is marked as atomic in the custom sync config, so it stays as is instead of becoming a LiveObject. - expectType<{ x: number; y: number }>(node.get("data").get("object")); - - // `array` is marked as local-only in the custom sync config, so it stays as is instead of becoming a LiveList and becomes optional. - expectType(node.get("data").get("array")); - - // `nested.nested` is marked as local-only in the custom sync config, so it stays as is instead of becoming a LiveObject and becomes optional. - expectType<{ array: string[] } | undefined>( - node.get("data").get("nested").get("nested") - ); - } - } -} - -/** - * LiveblocksEdge - */ -{ - const edge = {} as LiveblocksEdge; - - // The edge itself becomes a LiveObject. - expectAssignable>(edge); - - // Its root fields are "liveified" or not according to the base sync config. - { - // Synced fields are "liveified": scalars stay unchanged, objects become LiveObjects, and arrays become LiveLists. - expectAssignable | undefined>(edge.get("style")); - expectType(edge.get("id")); - - // `selected` is marked as local-only in the base sync config, so it becomes optional. - expectType(edge.get("selected")); - - // `labelBgPadding` is marked as atomic in the base sync config, so it stays as is instead of becoming a LiveObject. - expectType<[number, number] | undefined>(edge.get("labelBgPadding")); - } - - // The `data` field follows the sync config provided as a second generic. - { - // If no custom sync config is passed, everything is synced. - { - const edge = {} as LiveblocksEdge; + expectTypeOf(node.get("data")).toExtend>(); + + expectTypeOf(node.get("data").get("string")).toEqualTypeOf(); + expectTypeOf(node.get("data").get("nested")).toExtend< + LiveObject + >(); + + expectTypeOf(node.get("data").get("object")).toEqualTypeOf<{ + x: number; + y: number; + }>(); + + expectTypeOf(node.get("data").get("array")).toEqualTypeOf< + string[] | undefined + >(); - // On React Flow’s `Edge`, `data` is optional, so the LiveObject reflects that. - expectAssignable | undefined>(edge.get("data")); + expectTypeOf(node.get("data").get("nested").get("nested")).toEqualTypeOf< + { array: string[] } | undefined + >(); + }); + }); +}); - // Synced fields are "liveified": scalars stay unchanged, objects become LiveObjects, and arrays become LiveLists. - expectType(edge.get("data")!.get("string")); - expectAssignable>(edge.get("data")!.get("array")); - expectAssignable>(edge.get("data")!.get("nested")); - expectAssignable>( - edge.get("data")!.get("nested").get("nested")! - ); - } +describe("LiveblocksEdge", () => { + test("should reflect LiveObject shape and base sync config", () => { + const edge = {} as LiveblocksEdge; + + expectTypeOf(edge).toExtend>(); + + expectTypeOf(edge.get("style")).toExtend< + LiveObject | undefined + >(); + expectTypeOf(edge.get("id")).toEqualTypeOf(); + + expectTypeOf(edge.get("selected")).toEqualTypeOf(); + + expectTypeOf(edge.get("labelBgPadding")).toEqualTypeOf< + [number, number] | undefined + >(); + }); + + describe("data field (default sync)", () => { + test("should liveify optional edge data when no custom sync config is passed", () => { + const edge = {} as LiveblocksEdge; - // If a custom sync config is passed, it applies to the types as expected. - { + expectTypeOf(edge.get("data")).toExtend< + LiveObject | undefined + >(); + + expectTypeOf(edge.get("data")!.get("string")).toEqualTypeOf(); + expectTypeOf(edge.get("data")!.get("array")).toExtend>(); + expectTypeOf(edge.get("data")!.get("nested")).toExtend< + LiveObject + >(); + expectTypeOf(edge.get("data")!.get("nested").get("nested")!).toExtend< + LiveObject + >(); + }); + }); + + describe("data field (custom sync config)", () => { + test("should apply atomic and local-only overrides from sync config", () => { const edge = {} as LiveblocksEdge< CustomEdge, { object: "atomic"; array: false; nested: { nested: false } } >; - // On React Flow’s `Edge`, `data` is optional, so the LiveObject reflects that. - expectAssignable | undefined>(edge.get("data")); - - // Fields not present in the custom sync config are still synced, so they are still "liveified". - expectType(edge.get("data")!.get("string")); - expectAssignable>(edge.get("data")!.get("nested")); - - // `object` is marked as atomic in the custom sync config, so it stays as is instead of becoming a LiveObject. - expectType<{ x: number; y: number }>(edge.get("data")!.get("object")); - - // `array` is marked as local-only in the custom sync config, so it stays as is instead of becoming a LiveList and becomes optional. - expectType(edge.get("data")!.get("array")); - - // `nested.nested` is marked as local-only in the custom sync config, so it stays as is instead of becoming a LiveObject and becomes optional. - expectType<{ array: string[] } | undefined>( - edge.get("data")!.get("nested").get("nested") - ); - } - } -} - -/** - * LiveblocksFlow - */ -{ - const flow = {} as LiveblocksFlow; - - // A flow is a LiveObject containing nodes and edges as LiveMaps. - expectAssignable>(flow); - expectAssignable>>( - flow.get("nodes") - ); - expectAssignable>>( - flow.get("edges") - ); -} + expectTypeOf(edge.get("data")).toExtend< + LiveObject | undefined + >(); + + expectTypeOf(edge.get("data")!.get("string")).toEqualTypeOf(); + expectTypeOf(edge.get("data")!.get("nested")).toExtend< + LiveObject + >(); + + expectTypeOf(edge.get("data")!.get("object")).toEqualTypeOf<{ + x: number; + y: number; + }>(); + + expectTypeOf(edge.get("data")!.get("array")).toEqualTypeOf< + string[] | undefined + >(); + + expectTypeOf(edge.get("data")!.get("nested").get("nested")).toEqualTypeOf< + { array: string[] } | undefined + >(); + }); + }); +}); + +describe("LiveblocksFlow", () => { + test("should expose nodes and edges as LiveMaps", () => { + const flow = {} as LiveblocksFlow; + + expectTypeOf(flow).toExtend>(); + expectTypeOf(flow.get("nodes")).toExtend< + LiveMap> + >(); + expectTypeOf(flow.get("edges")).toExtend< + LiveMap> + >(); + }); +}); diff --git a/packages/liveblocks-react-flow/vitest.config.typecheck.ts b/packages/liveblocks-react-flow/vitest.config.typecheck.ts new file mode 100644 index 00000000000..8bdcb63a0e0 --- /dev/null +++ b/packages/liveblocks-react-flow/vitest.config.typecheck.ts @@ -0,0 +1,6 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: makeTypecheckTestConfig(import.meta, ["test-d/**/*.test-d.ts"]), +}); diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json index 98a8c895837..501fe0d712d 100644 --- a/packages/liveblocks-react-lexical/package.json +++ b/packages/liveblocks-react-lexical/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-lexical", - "version": "3.18.0", + "version": "3.18.1", "description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -45,11 +45,11 @@ }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "radix-ui": "^1.4.0", "yjs": "^13.6.18" }, diff --git a/packages/liveblocks-react-lexical/test-d/index.test-d.ts b/packages/liveblocks-react-lexical/test-d/index.test-d.ts deleted file mode 100644 index 6dd9794e170..00000000000 --- a/packages/liveblocks-react-lexical/test-d/index.test-d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// import * as lbrl from "@liveblocks/react-lexical"; -import { expectType } from "tsd"; - -(async function main() { - expectType("hi".toString()); - - // TODO: Write type-level tests for these exports - // expectType(lbrl.FloatingComposer); - // expectType(lbrl.FloatingThreads); - // expectType(lbrl.liveblocksConfig); - // expectType(lbrl.LiveblocksPlugin); - // expectType(lbrl.Mention); - // expectType(lbrl.OPEN_FLOATING_COMPOSER_COMMAND); - // expectType(lbrl.AnchoredThreads); -})(); diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json index 1c37b689e69..dc204a42d98 100644 --- a/packages/liveblocks-react-tiptap/package.json +++ b/packages/liveblocks-react-tiptap/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-tiptap", - "version": "3.18.0", + "version": "3.18.1", "description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -45,11 +45,11 @@ }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", - "@liveblocks/react-ui": "3.18.0", - "@liveblocks/yjs": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", + "@liveblocks/react-ui": "3.18.1", + "@liveblocks/yjs": "3.18.1", "@tiptap/core": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/suggestion": "^3.19.0", diff --git a/packages/liveblocks-react-tiptap/test-d/index.test-d.ts b/packages/liveblocks-react-tiptap/test-d/index.test-d.ts deleted file mode 100644 index 4d6fe194844..00000000000 --- a/packages/liveblocks-react-tiptap/test-d/index.test-d.ts +++ /dev/null @@ -1,12 +0,0 @@ -// import * as lbrl from "@liveblocks/react-tiptap"; -import { expectType } from "tsd"; - -(async function main() { - expectType("hi".toString()); - - // TODO: Write type-level tests for these exports - // expectType(lbrl.FloatingComposer); - // expectType(lbrl.FloatingThreads); - // expectType(lbrl.LiveblocksExtension); - // expectType(lbrl.AnchoredThreads); -})(); diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json index de725afc3c8..ee86159b85e 100644 --- a/packages/liveblocks-react-ui/package.json +++ b/packages/liveblocks-react-ui/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-ui", - "version": "3.18.0", + "version": "3.18.1", "description": "A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -73,14 +73,14 @@ "lint:package": "publint --strict && attw --pack", "test": "npx liveblocks dev -p 1154 -c 'vitest run'", "test:ci": "vitest run", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "vitest" }, "dependencies": { "@floating-ui/react-dom": "^2.1.0", - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", - "@liveblocks/react": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", + "@liveblocks/react": "3.18.1", "frimousse": "^0.2.0", "marked": "^15.0.11", "radix-ui": "^1.4.0", diff --git a/packages/liveblocks-react-ui/src/primitives/Composer/slate/plugins/mentions.ts b/packages/liveblocks-react-ui/src/primitives/Composer/slate/plugins/mentions.ts index 22f4736c917..50ba728485e 100644 --- a/packages/liveblocks-react-ui/src/primitives/Composer/slate/plugins/mentions.ts +++ b/packages/liveblocks-react-ui/src/primitives/Composer/slate/plugins/mentions.ts @@ -16,6 +16,27 @@ import { getMatchRange } from "../../../slate/utils/get-match-range"; import { isEmptyString } from "../../../slate/utils/is-empty-string"; import { isWhitespaceCharacter } from "../../../slate/utils/is-whitespace-character"; +const EMOJI_REGEX = + /\p{Extended_Pictographic}|\p{Emoji_Modifier}|\uFE0F|\u200D/u; + +// A few punctuation characters that aren't common (or even forbidden) as the +// last character in email addresses' local part so we allow them before "@" mentions. +const SUPPORTED_PRECEDING_PUNCTUATION = new Set([ + '"', + "'", + ".", + "!", + "?", + ",", + ";", + "(", + ")", + "[", + "]", + "<", + ">", +]); + export type MentionDraft = { range: SlateRange; text: string; @@ -30,20 +51,38 @@ export function getMentionDraftAtSelection( return; } - // Walk backwards from the selection until "@" is found, unless the character - // before isn't whitespace (or "@" is the block's first character) + // Walk backwards from the selection until "@" is found (with a few rules): + // - `@` → `""` mention draft + // - `Hello @stacy` → `"stacy"` mention draft (because "@" is preceded by a whitespace character) + // - `Hello pierre@example.com` → no mention draft (because "@" is preceded by a non-whitespace character) + // - `Hello @alicia@example.com` → `"alicia@example.com"` mention draft (because the first "@" is preceded by a whitespace character) + // - `Hello!@chris` → `"chris"` mention draft (because "!" is a supported preceding punctuation) + // - `Hello.@vincent` → `"vincent"` mention draft (because "." is a supported preceding punctuation) + // - `Hello (@olivier` → `"olivier"` mention draft (because "(" is a supported preceding punctuation) + // - `Hello 👋@nimesh` → `"nimesh"` mention draft (because the first "@" is preceded by an emoji) const match = getMatchRange(editor, selection, ["@"], { include: true, allowConsecutiveWhitespace: false, ignoreTerminator: (_, point) => { const characterBefore = getCharacterBefore(editor, point); - // Ignore "@" if it's preceded by a non-whitespace character - if (characterBefore && !isWhitespaceCharacter(characterBefore.text)) { - return true; + // If "@" is the first character of the block, this "@" is immediately accepted ✅ + if (!characterBefore) { + return false; + } + + // If the character before "@" is a supported preceding punctuation, this "@" is accepted ✅ + if (SUPPORTED_PRECEDING_PUNCTUATION.has(characterBefore.text)) { + return false; + } + + // If the character before "@" is an emoji, this "@" is accepted ✅ + if (EMOJI_REGEX.test(characterBefore.text)) { + return false; } - return false; + // If the character before "@" is still a non-whitespace character, this "@" is ignored ❌ + return !isWhitespaceCharacter(characterBefore.text); }, }); diff --git a/packages/liveblocks-react-ui/test-d/augmentation.test-d.tsx b/packages/liveblocks-react-ui/test-d/augmentation.test-d.tsx index 51b08d70fb0..aae5065d8ed 100644 --- a/packages/liveblocks-react-ui/test-d/augmentation.test-d.tsx +++ b/packages/liveblocks-react-ui/test-d/augmentation.test-d.tsx @@ -4,9 +4,9 @@ import type { LiveMap, LiveObject, } from "@liveblocks/core"; -import { expectError, expectType } from "tsd"; import type { InboxNotificationCustomKindProps } from "@liveblocks/react-ui"; import { InboxNotification } from "@liveblocks/react-ui"; +import { describe, expectTypeOf, test } from "vitest"; // TODO: Create type tests for all components/props @@ -65,66 +65,76 @@ declare global { } } -// : `kinds` prop -{ - // Existing kinds have the expected props types - { - { - expectType(props.inboxNotification.threadId); - expectError(props.inboxNotification.activities); - - return ; - }, - textMention: (props) => { - expectType(props.inboxNotification.mentionId); - expectError(props.inboxNotification.activities); - - return ; - }, - $myErrorNotification: (props) => { - expectType( - props.inboxNotification.activities[0]!.data.message - ); - expectType(props.inboxNotification.activities[0]!.data.code); - expectError(props.inboxNotification.activities[0]!.data.nonexisting); - expectError(props.inboxNotification.threadId); - expectError(props.inboxNotification.mentionId); - - return ( - - Content - - ); - }, - }} - />; - } +describe("InboxNotification `kinds` (with Liveblocks augmentation)", () => { + test("existing kinds have the expected props types", () => { + void ( + { + expectTypeOf( + props.inboxNotification.threadId + ).toEqualTypeOf(); + // @ts-expect-error - `activities` is not on thread notifications + void props.inboxNotification.activities; + + return ; + }, + textMention: (props) => { + expectTypeOf( + props.inboxNotification.mentionId + ).toEqualTypeOf(); + // @ts-expect-error - `activities` is not on text-mention notifications + void props.inboxNotification.activities; + + return ; + }, + $myErrorNotification: (props) => { + expectTypeOf( + props.inboxNotification.activities[0]!.data.message + ).toEqualTypeOf(); + expectTypeOf( + props.inboxNotification.activities[0]!.data.code + ).toEqualTypeOf(); + // @ts-expect-error - invalid activity data field + void props.inboxNotification.activities[0]!.data.nonexisting; + // @ts-expect-error - `threadId` is not on this custom kind + void props.inboxNotification.threadId; + // @ts-expect-error - `mentionId` is not on this custom kind + void props.inboxNotification.mentionId; + + return ( + + Content + + ); + }, + }} + /> + ); + }); - // Non-existing custom kinds are not allowed - { - expectError( + test("rejects custom kind keys that are not declared in ActivitiesData", () => { + void ( null, }} /> ); - } + }); - // Custom kinds’ props types can be used as expected in external components - { + test("custom kind props types work in external components", () => { function MyCustomInboxNotification({ inboxNotification, ...props }: InboxNotificationCustomKindProps) { - expectType< + expectTypeOf(inboxNotification.activities[0]!.data).toEqualTypeOf< | { message: string; code: number } | { duration: number; uploadId: string } - >(inboxNotification.activities[0]!.data); + >(); return ( ) { - expectType(inboxNotification.activities[0]!.data.message); - expectType(inboxNotification.activities[0]!.data.code); - expectError(inboxNotification.activities[0]!.data.nonexisting); + expectTypeOf( + inboxNotification.activities[0]!.data.message + ).toEqualTypeOf(); + expectTypeOf( + inboxNotification.activities[0]!.data.code + ).toEqualTypeOf(); + // @ts-expect-error - invalid activity data field + void inboxNotification.activities[0]!.data.nonexisting; return ( ; + void ( + + ); - // When ActivitiesData is set with augmentation, specific custom kinds’ props types can only be used with their kinds - expectError( + void ( ); - } -} + }); +}); diff --git a/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx b/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx index fc8171ff76c..40607d677ff 100644 --- a/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx +++ b/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx @@ -1,61 +1,73 @@ import type { ActivityData, InboxNotificationData } from "@liveblocks/core"; -import { expectError, expectType } from "tsd"; import type { InboxNotificationCustomKindProps } from "@liveblocks/react-ui"; import { InboxNotification } from "@liveblocks/react-ui"; +import { describe, expectTypeOf, test } from "vitest"; // TODO: Create type tests for all components/props -// : `kinds` prop -{ - // Existing kinds have the expected props types - { - { - expectType(props.inboxNotification.threadId); - expectError(props.inboxNotification.activities); +describe("InboxNotification `kinds` (no Liveblocks augmentation)", () => { + test("existing kinds have the expected props types", () => { + void ( + { + expectTypeOf( + props.inboxNotification.threadId + ).toEqualTypeOf(); + // @ts-expect-error - `activities` is not on thread notifications + void props.inboxNotification.activities; - return ; - }, - textMention: (props) => { - expectType(props.inboxNotification.mentionId); - expectError(props.inboxNotification.activities); + return ; + }, + textMention: (props) => { + expectTypeOf( + props.inboxNotification.mentionId + ).toEqualTypeOf(); + // @ts-expect-error - `activities` is not on text-mention notifications + void props.inboxNotification.activities; - return ; - }, - $myErrorNotification: (props) => { - expectType(props.inboxNotification.activities[0]!.data); - expectError(props.inboxNotification.threadId); - expectError(props.inboxNotification.mentionId); + return ; + }, + $myErrorNotification: (props) => { + expectTypeOf( + props.inboxNotification.activities[0]!.data + ).toEqualTypeOf(); + // @ts-expect-error - `threadId` is not on this custom kind + void props.inboxNotification.threadId; + // @ts-expect-error - `mentionId` is not on this custom kind + void props.inboxNotification.mentionId; - return ( - - Content - - ); - }, - }} - />; - } + return ( + + Content + + ); + }, + }} + /> + ); + }); - // All custom kinds are allowed when ActivitiesData is not set with augmentation - { - null, - }} - />; - } + test("allows arbitrary custom kind keys when ActivitiesData is not augmented", () => { + void ( + null, + }} + /> + ); + }); - // Custom kinds’ props types can be used as expected in external components - { + test("custom kind props types work in external components", () => { function MyCustomInboxNotification({ inboxNotification, ...props }: InboxNotificationCustomKindProps) { - expectType(inboxNotification.activities[0]!.data); + expectTypeOf( + inboxNotification.activities[0]!.data + ).toEqualTypeOf(); return ( ) { - expectType(inboxNotification.activities[0]!.data); + expectTypeOf( + inboxNotification.activities[0]!.data + ).toEqualTypeOf(); return ( ; + void ( + + ); - expectError( + void ( ); - } -} + }); +}); diff --git a/packages/liveblocks-react-ui/vitest.config.typecheck.ts b/packages/liveblocks-react-ui/vitest.config.typecheck.ts new file mode 100644 index 00000000000..21f4dde954f --- /dev/null +++ b/packages/liveblocks-react-ui/vitest.config.typecheck.ts @@ -0,0 +1,25 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +// Multiple configs are used when testing augmentation and global +// declerations, to avoid leaking globals between tests. +export default defineConfig({ + test: { + projects: [ + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/augmentation.test-d.tsx"], + "augmentation" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/no-augmentation.test-d.tsx"], + "no-augmentation" + ), + }, + ], + }, +}); diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json index d80a485dd41..c8c9eee631e 100644 --- a/packages/liveblocks-react/package.json +++ b/packages/liveblocks-react/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react", - "version": "3.18.0", + "version": "3.18.1", "description": "A set of React hooks and providers to use Liveblocks declaratively. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -57,14 +57,14 @@ "lint:package": "publint --strict && attw --pack && bun scripts/check-exports.ts", "test": "NODE_OPTIONS=\"--no-deprecation\" jest --silent --verbose --color=always", "test:ci": "NODE_OPTIONS=\"--no-deprecation\" jest --silent --verbose --color=always", - "test:types": "ls test-d/* | xargs -n1 tsd --files", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "NODE_OPTIONS=\"--no-deprecation\" jest --silent --verbose --color=always --watch", "test:deps": "depcruise src --exclude __tests__", "showdeps": "depcruise src --include-only '^src' --exclude='__tests__' --output-type dot | dot -T svg > /tmp/dependency-graph.svg && open /tmp/dependency-graph.svg" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "peerDependencies": { "@types/react": "*", @@ -82,6 +82,7 @@ "devDependencies": { "@liveblocks/eslint-config": "*", "@liveblocks/jest-config": "*", + "@liveblocks/vitest-config": "*", "@liveblocks/query-parser": "^0.1.1", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "14.1.2", diff --git a/packages/liveblocks-react/test-d/augmentation.test-d.tsx b/packages/liveblocks-react/test-d/augmentation.test-d.tsx index dee72a6cf94..f94bbecedcf 100644 --- a/packages/liveblocks-react/test-d/augmentation.test-d.tsx +++ b/packages/liveblocks-react/test-d/augmentation.test-d.tsx @@ -1,8 +1,9 @@ +import React from "react"; import type { NotificationSettings } from "@liveblocks/core"; import { LiveList, LiveMap, LiveObject } from "@liveblocks/core"; import * as classic from "@liveblocks/react"; import * as suspense from "@liveblocks/react/suspense"; -import { expectAssignable, expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; // // User-provided type augmentations @@ -54,1276 +55,1478 @@ declare global { } } -// LiveblocksProvider -{ - const LiveblocksProvider = classic.LiveblocksProvider; - - expectError(); - expectError(); - - ; - ; - ; - ({ token: "token" })} - throttle={16} - />; - - expectError( - [{ foo: "bar" }]} - /> - ); - - [{ name: "Vincent", age: 42 }]} - />; - - ; -} - -// LiveblocksProvider (suspense) -{ - const LiveblocksProvider = suspense.LiveblocksProvider; - - expectError(); - expectError(); - - ; - ; - ; - ({ token: "token" })} - throttle={16} - />; - - expectError( - [{ foo: "bar" }]} - /> - ); - - [{ name: "Vincent", age: 42 }]} - />; - - ; -} - -// RoomProvider -{ - const RoomProvider = classic.RoomProvider; - - // Missing mandatory props is an error - expectError( - -
- - ); - - expectError( - // Missing initialPresence is an error - -
- - ); - - expectError( - // Missing mandatory initialStorage is an error - -
- - ); - - expectError( - // Missing mandatory initialPresence is an error - -
- - ); - - expectError( - // Missing mandatory initialStorage - -
- - ); - - expectError( - -
- - ); - - -
- ; -} - -// RoomProvider (suspense) -{ - const RoomProvider = suspense.RoomProvider; - - // Missing mandatory props is an error - expectError( - -
- - ); - - expectError( - // Missing initialPresence is an error - -
- - ); - - expectError( - // Missing mandatory initialStorage is an error - -
- - ); - - expectError( - // Missing mandatory initialPresence is an error - -
- - ); - - expectError( - // Missing mandatory initialStorage - -
- - ); - - expectError( - -
- - ); - - -
- ; -} - -// --------------------------------------------------------- -// Hook APIs -// --------------------------------------------------------- - -// useRoom() -{ - const room = classic.useRoom(); - expectType(room.getPresence().cursor.x); - expectType(room.getPresence().cursor.y); - expectError(room.getPresence().nonexisting); - - expectType(classic.useRoom({ allowOutsideRoom: false }).id); - expectType( - classic.useRoom({ allowOutsideRoom: true })?.id - ); - expectType( - classic.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id - ); -} - -// useRoom() (suspense) -{ - const room = suspense.useRoom(); - expectType(room.getPresence().cursor.x); - expectType(room.getPresence().cursor.y); - expectError(room.getPresence().nonexisting); - - expectType(suspense.useRoom({ allowOutsideRoom: false }).id); - expectType( - suspense.useRoom({ allowOutsideRoom: true })?.id - ); - expectType( - suspense.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id - ); -} - -// --------------------------------------------------------- - -// useIsInsideRoom() -{ - const isInsideRoom = classic.useIsInsideRoom(); - expectType(isInsideRoom); -} - -// useIsInsideRoom() (suspense) -{ - const isInsideRoom = suspense.useIsInsideRoom(); - expectType(isInsideRoom); -} - -// --------------------------------------------------------- - -// useErrorListener() -{ - classic.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... +describe("with Liveblocks augmentation", () => { + test("LiveblocksProvider", () => { + { + const LiveblocksProvider = classic.LiveblocksProvider; + + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + + ; + ; + ; + ({ token: "token" })} + throttle={16} + />; + + void ( + [{ foo: "bar" }]} + /> + ); + + [{ name: "Vincent", age: 42 }]} + />; + + ; } }); -} -// useErrorListener() (suspense) -{ - suspense.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... + test("LiveblocksProvider (suspense)", () => { + { + const LiveblocksProvider = suspense.LiveblocksProvider; + + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + + ; + ; + ; + ({ token: "token" })} + throttle={16} + />; + + void ( + [{ foo: "bar" }]} + /> + ); + + [{ name: "Vincent", age: 42 }]} + />; + + ; } }); -} - -// --------------------------------------------------------- - -// useSelf() -{ - const me = classic.useSelf(); - expectType(me?.presence.cursor.x); - expectType(me?.presence.cursor.y); - expectError(me?.presence.nonexisting); - - expectType(me?.info.name); - expectType(me?.info.age); - expectError(me?.info.nonexisting); -} - -// useSelf() (suspense) -{ - const me = suspense.useSelf(); - expectType(me.presence.cursor.x); - expectType(me.presence.cursor.y); - expectError(me.presence.nonexisting); - - expectType(me.info.name); - expectType(me.info.age); - expectError(me.info.nonexisting); -} - -// useSelf(selector) -{ - const x = classic.useSelf((me) => me.presence.cursor.x); - expectType(x); -} -// useSelf(selector) (suspense) -{ - const x = suspense.useSelf((me) => me.presence.cursor.x); - expectType(x); -} - -// --------------------------------------------------------- - -// useOthers() -{ - const others = classic.useOthers(); - expectType(others[13]!.presence.cursor.x); - expectType(others[42]!.presence.cursor.y); - expectType(others[0]!.canWrite); -} - -// useOthers() (suspense) -{ - const others = suspense.useOthers(); - expectType(others[13]!.presence.cursor.x); - expectType(others[42]!.presence.cursor.y); - expectType(others[0]!.canWrite); -} - -// useOthers(selector) -{ - const num = classic.useOthers((others) => others.length); - expectType(num); - - const xs = classic.useOthers((others) => - others.map((o) => o.presence.cursor.x) - ); - expectType(xs); -} + test("RoomProvider", () => { + { + const RoomProvider = classic.RoomProvider; + + // Missing mandatory props is an error + void ( + ( + // @ts-expect-error + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing initialPresence + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + +
+ + ); + + +
+ ; + } + }); -// useOthers(selector) (suspense) -{ - const num = classic.useOthers((others) => others.length); - expectType(num); + test("RoomProvider (suspense)", () => { + { + const RoomProvider = suspense.RoomProvider; + + // Missing mandatory props is an error + void ( + ( + // @ts-expect-error + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing initialPresence + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + +
+ + ); + + +
+ ; + } + }); - const xs = classic.useOthers((others) => - others.map((o) => o.presence.cursor.x) - ); - expectType(xs); -} + test("useRoom()", () => { + { + const room = classic.useRoom(); + expectTypeOf(room.getPresence().cursor.x).toEqualTypeOf(); + expectTypeOf(room.getPresence().cursor.y).toEqualTypeOf(); + // @ts-expect-error + void room.getPresence().nonexisting; + + expectTypeOf( + classic.useRoom({ allowOutsideRoom: false }).id + ).toEqualTypeOf(); + expectTypeOf( + classic.useRoom({ allowOutsideRoom: true })?.id + ).toEqualTypeOf(); + expectTypeOf( + classic.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id + ).toEqualTypeOf(); + } + }); -// useOthers(selector, eq) -{ - const xs = classic.useOthers( - (others) => others.map((o) => o.presence.cursor.x), - classic.shallow - ); - expectType(xs); -} + test("useRoom() (suspense)", () => { + { + const room = suspense.useRoom(); + expectTypeOf(room.getPresence().cursor.x).toEqualTypeOf(); + expectTypeOf(room.getPresence().cursor.y).toEqualTypeOf(); + // @ts-expect-error + void room.getPresence().nonexisting; + + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: false }).id + ).toEqualTypeOf(); + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: true })?.id + ).toEqualTypeOf(); + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id + ).toEqualTypeOf(); + } + }); -// useOthers(selector, eq) (suspense) -{ - const xs = suspense.useOthers( - (others) => others.map((o) => o.presence.cursor.x), - suspense.shallow - ); - expectType(xs); -} + test("useIsInsideRoom()", () => { + { + const isInsideRoom = classic.useIsInsideRoom(); + expectTypeOf(isInsideRoom).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- - -// The useMutation() hook -{ - expectType<(a: number, b: boolean) => "hi">( - classic.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor.x); - expectError(mut.self.presence.nonexisting); - expectType(mut.self.info.name); - expectType(mut.self.info.age); - expectError(mut.self.info.nonexisting); - - expectType(mut.others[0]!.presence.cursor.x); - expectError(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info.name); - expectType(mut.others[0]!.info.age); - expectError(mut.others[0]!.info.nonexisting); - - expectType(mut.storage.get("animals").get(0)); - expectType(mut.storage.get("scores").get("one")); - expectType(mut.storage.get("person").get("age")); - expectError(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectError(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} + test("useIsInsideRoom() (suspense)", () => { + { + const isInsideRoom = suspense.useIsInsideRoom(); + expectTypeOf(isInsideRoom).toEqualTypeOf(); + } + }); -// The useMutation() hook (suspense) -{ - expectType<(a: number, b: boolean) => "hi">( - suspense.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor.x); - expectError(mut.self.presence.nonexisting); - expectType(mut.self.info.name); - expectType(mut.self.info.age); - expectError(mut.self.info.nonexisting); - - expectType(mut.others[0]!.presence.cursor.x); - expectError(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info.name); - expectType(mut.others[0]!.info.age); - expectError(mut.others[0]!.info.nonexisting); - - expectType(mut.storage.get("animals").get(0)); - expectType(mut.storage.get("scores").get("one")); - expectType(mut.storage.get("person").get("age")); - expectError(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectError(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} + test("useErrorListener()", () => { + { + classic.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + } + }); -// --------------------------------------------------------- - -// useBroadcastEvent() -{ - const broadcast = classic.useBroadcastEvent(); - broadcast({ type: "emoji", emoji: "😍" }); - broadcast({ type: "beep", times: 3 }); - broadcast({ type: "beep" }); - // broadcast({ type: "leave", userId: "1234" }); // TODO Allow this using union types - expectError(broadcast({ type: "i-do-not-exist" })); - expectError(broadcast(new Date())); -} + test("useErrorListener() (suspense)", () => { + { + suspense.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + } + }); -// useBroadcastEvent() (suspense) -{ - const broadcast = suspense.useBroadcastEvent(); - broadcast({ type: "emoji", emoji: "😍" }); - broadcast({ type: "beep", times: 3 }); - broadcast({ type: "beep" }); - // broadcast({ type: "leave", userId: "1234" }); // TODO Allow this using union types - expectError(broadcast({ type: "i-do-not-exist" })); - expectError(broadcast(new Date())); -} + test("useSelf()", () => { + { + const me = classic.useSelf(); + expectTypeOf(me?.presence.cursor.x).toEqualTypeOf(); + expectTypeOf(me?.presence.cursor.y).toEqualTypeOf(); + // @ts-expect-error + void me?.presence.nonexisting; + + expectTypeOf(me?.info.name).toEqualTypeOf(); + expectTypeOf(me?.info.age).toEqualTypeOf(); + // @ts-expect-error + void me?.info.nonexisting; + } + }); -// --------------------------------------------------------- - -// The useUser() hook -{ - const { user, error, isLoading } = classic.useUser("user-id"); - expectType(isLoading); - expectType(user!.name); - expectError(user?.avatar); - expectType(user!.age); - expectError(user?.anyOtherProp); - expectType(error); -} + test("useSelf() (suspense)", () => { + { + const me = suspense.useSelf(); + expectTypeOf(me.presence.cursor.x).toEqualTypeOf(); + expectTypeOf(me.presence.cursor.y).toEqualTypeOf(); + // @ts-expect-error + void me.presence.nonexisting; + + expectTypeOf(me.info.name).toEqualTypeOf(); + expectTypeOf(me.info.age).toEqualTypeOf(); + // @ts-expect-error + void me.info.nonexisting; + } + }); -// The useUser() hook (suspense) -{ - const { user, error, isLoading } = suspense.useUser("user-id"); - expectType(isLoading); - expectType(user.name); - expectError(user.avatar); - expectType(user.age); - expectError(user.anyOtherProp); - expectType(error); -} + test("useSelf(selector)", () => { + { + const x = classic.useSelf((me) => me.presence.cursor.x); + expectTypeOf(x).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- - -// The useRoomInfo() hook -{ - const { info, error, isLoading } = classic.useRoomInfo("room-id"); - expectType(isLoading); - expectType(info!.name); - expectType(info!.url); - expectType<"public" | "private" | undefined>(info?.type); - expectError(info?.nonexisting); - expectType(error); -} + test("useSelf(selector) (suspense)", () => { + { + const x = suspense.useSelf((me) => me.presence.cursor.x); + expectTypeOf(x).toEqualTypeOf(); + } + }); -// The useRoomInfo() hook (suspense) -{ - const { info, error, isLoading } = suspense.useRoomInfo("room-id"); - expectType(isLoading); - expectType(info.name); - expectType(info.url); - expectType<"public" | "private">(info?.type); - expectError(info?.nonexisting); - expectType(error); -} + test("useOthers()", () => { + { + const others = classic.useOthers(); + expectTypeOf(others[13]!.presence.cursor.x).toEqualTypeOf(); + expectTypeOf(others[42]!.presence.cursor.y).toEqualTypeOf(); + expectTypeOf(others[0]!.canWrite).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- - -// The useGroupInfo() hook -{ - const { info, error, isLoading } = classic.useGroupInfo("group-id"); - expectType(isLoading); - expectType(info!.name); - expectType(info!.avatar); - expectType<"open" | "closed" | undefined>(info?.type); - expectError(info?.nonexisting); - expectType(error); -} + test("useOthers() (suspense)", () => { + { + const others = suspense.useOthers(); + expectTypeOf(others[13]!.presence.cursor.x).toEqualTypeOf(); + expectTypeOf(others[42]!.presence.cursor.y).toEqualTypeOf(); + expectTypeOf(others[0]!.canWrite).toEqualTypeOf(); + } + }); -// The useGroupInfo() hook (suspense) -{ - const { info, error, isLoading } = suspense.useGroupInfo("group-id"); - expectType(isLoading); - expectType(info.name); - expectType(info.avatar); - expectType<"open" | "closed">(info?.type); - expectError(info?.nonexisting); - expectType(error); -} + test("useOthers(selector)", () => { + { + const num = classic.useOthers((others) => others.length); + expectTypeOf(num).toEqualTypeOf(); -// --------------------------------------------------------- - -// The useCreateThread() hook -{ - const createThread = classic.useCreateThread(); - expectError(createThread({})); // no body = error - - // No metadata = error - expectError( - createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }) - ); - - const thread = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { color: "red" }, - commentMetadata: { priority: 1 }, - }); - - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType<"comment">(thread.comments[0]!.type); - expectType(thread.comments[0]!.id); - expectType(thread.comments[0]!.threadId); - - expectType<"red" | "blue">(thread.metadata.color); - expectError(thread.metadata.nonexisting); -} + const xs = classic.useOthers((others) => + others.map((o) => o.presence.cursor.x) + ); + expectTypeOf(xs).toEqualTypeOf(); + } + }); -// The useCreateThread() hook (suspense) -{ - const createThread = suspense.useCreateThread(); - expectError(createThread({})); // no body = error - - // No metadata = error - expectError( - createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }) - ); - - const thread = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { color: "red" }, - commentMetadata: { priority: 1 }, - }); - - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType<"comment">(thread.comments[0]!.type); - expectType(thread.comments[0]!.id); - expectType(thread.comments[0]!.threadId); - - expectType<"red" | "blue">(thread.metadata.color); - expectError(thread.metadata.nonexisting); -} + test("useOthers(selector) (suspense)", () => { + { + const num = classic.useOthers((others) => others.length); + expectTypeOf(num).toEqualTypeOf(); -// --------------------------------------------------------- - -// The useEditThreadMetadata() hook -{ - const editMetadata = classic.useEditThreadMetadata(); - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ - threadId: "th_xxx", - metadata: { color: "red", pinned: null }, - }) - ); - - expectError(editMetadata({ threadId: "th_xxx", metadata: { color: null } })); // Color isn't optional, so cannot be wiped -} + const xs = classic.useOthers((others) => + others.map((o) => o.presence.cursor.x) + ); + expectTypeOf(xs).toEqualTypeOf(); + } + }); -// The useEditThreadMetadata() hook (suspense) -{ - const editMetadata = suspense.useEditThreadMetadata(); - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ - threadId: "th_xxx", - metadata: { color: "red", pinned: null }, - }) - ); - - expectError(editMetadata({ threadId: "th_xxx", metadata: { color: null } })); // Color isn't optional, so cannot be wiped -} + test("useOthers(selector, eq)", () => { + { + const xs = classic.useOthers( + (others) => others.map((o) => o.presence.cursor.x), + classic.shallow + ); + expectTypeOf(xs).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- - -// The useCreateComment() hook -{ - { - const createComment = classic.useCreateComment(); - expectError(createComment({})); - - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { - priority: 2, - reviewed: false, - }, - }); - - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.metadata.priority); - expectType(comment.metadata.reviewed); - expectError(comment.metadata.nonexisting); - } -} + test("useOthers(selector, eq) (suspense)", () => { + { + const xs = suspense.useOthers( + (others) => others.map((o) => o.presence.cursor.x), + suspense.shallow + ); + expectTypeOf(xs).toEqualTypeOf(); + } + }); -// The useCreateComment() hook (suspense) -{ - const createComment = suspense.useCreateComment(); - expectError(createComment({})); + test("useMutation()", () => { + { + expectTypeOf( + classic.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void mut.self.presence.nonexisting; + expectTypeOf(mut.self.info.name).toEqualTypeOf(); + expectTypeOf(mut.self.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.self.info.nonexisting; + + expectTypeOf( + mut.others[0]!.presence.cursor.x + ).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.presence.nonexisting; + expectTypeOf(mut.others[0]!.info.name).toEqualTypeOf(); + expectTypeOf(mut.others[0]!.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.info.nonexisting; + + expectTypeOf(mut.storage.get("animals").get(0)).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.storage.get("scores").get("one")).toEqualTypeOf< + number | undefined + >(); + expectTypeOf( + mut.storage.get("person").get("age") + ).toEqualTypeOf(); + // @ts-expect-error + void mut.storage.get("nonexisting"); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + // @ts-expect-error + void mut.setMyPresence({ nonexisting: 123 }); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } + }); - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { priority: 1 }, + test("useMutation() (suspense)", () => { + { + expectTypeOf( + suspense.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void mut.self.presence.nonexisting; + expectTypeOf(mut.self.info.name).toEqualTypeOf(); + expectTypeOf(mut.self.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.self.info.nonexisting; + + expectTypeOf( + mut.others[0]!.presence.cursor.x + ).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.presence.nonexisting; + expectTypeOf(mut.others[0]!.info.name).toEqualTypeOf(); + expectTypeOf(mut.others[0]!.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.info.nonexisting; + + expectTypeOf(mut.storage.get("animals").get(0)).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.storage.get("scores").get("one")).toEqualTypeOf< + number | undefined + >(); + expectTypeOf( + mut.storage.get("person").get("age") + ).toEqualTypeOf(); + // @ts-expect-error + void mut.storage.get("nonexisting"); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + // @ts-expect-error + void mut.setMyPresence({ nonexisting: 123 }); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } }); - expectType<"comment">(comment.type); - expectType(comment.metadata.priority); - expectError(comment.metadata.nonexisting); -} + test("useBroadcastEvent()", () => { + { + const broadcast = classic.useBroadcastEvent(); + broadcast({ type: "emoji", emoji: "😍" }); + broadcast({ type: "beep", times: 3 }); + broadcast({ type: "beep" }); + // broadcast({ type: "leave", userId: "1234" }); // TODO Allow this using union types + // @ts-expect-error + void broadcast({ type: "i-do-not-exist" }); + // @ts-expect-error + void broadcast(new Date()); + } + }); -// --------------------------------------------------------- - -// The useEditComment() hook -{ - const editComment = classic.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2, reviewed: null }, - }) - ); - - expectError( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { nonexisting: null }, - }) - ); -} + test("useBroadcastEvent() (suspense)", () => { + { + const broadcast = suspense.useBroadcastEvent(); + broadcast({ type: "emoji", emoji: "😍" }); + broadcast({ type: "beep", times: 3 }); + broadcast({ type: "beep" }); + // broadcast({ type: "leave", userId: "1234" }); // TODO Allow this using union types + // @ts-expect-error + void broadcast({ type: "i-do-not-exist" }); + // @ts-expect-error + void broadcast(new Date()); + } + }); -// The useEditComment() hook (suspense) -{ - const editComment = suspense.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2 }, - }) - ); - - expectError( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { nonexisting: null }, - }) - ); -} + test("useUser()", () => { + { + const { user, error, isLoading } = classic.useUser("user-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user!.name).toEqualTypeOf(); + // @ts-expect-error + void user?.avatar; + expectTypeOf(user!.age).toEqualTypeOf(); + // @ts-expect-error + void user?.anyOtherProp; + expectTypeOf(error).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- - -// The useEditCommentMetadata() hook -{ - const editMetadata = classic.useEditCommentMetadata(); - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: null }, - }) - ); - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: 123 }, - }) - ); - - expectType( - editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { priority: 2, reviewed: null }, - }) - ); - - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { priority: null }, - }) - ); // priority isn't optional, so it cannot be wiped -} + test("useUser() (suspense)", () => { + { + const { user, error, isLoading } = suspense.useUser("user-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user.name).toEqualTypeOf(); + // @ts-expect-error + void user.avatar; + expectTypeOf(user.age).toEqualTypeOf(); + // @ts-expect-error + void user.anyOtherProp; + expectTypeOf(error).toEqualTypeOf(); + } + }); -// The useEditCommentMetadata() hook (suspense) -{ - const editMetadata = suspense.useEditCommentMetadata(); - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: null }, - }) - ); - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: 123 }, - }) - ); - - expectType( - editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { priority: 2, reviewed: null }, - }) - ); - - expectError( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { priority: null }, - }) - ); // priority isn't optional, so it cannot be wiped -} + test("useRoomInfo()", () => { + { + const { info, error, isLoading } = classic.useRoomInfo("room-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info!.name).toEqualTypeOf(); + expectTypeOf(info!.url).toEqualTypeOf(); + expectTypeOf(info?.type).toEqualTypeOf< + "public" | "private" | undefined + >(); + // @ts-expect-error + void info?.nonexisting; + expectTypeOf(error).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useRoomInfo() (suspense)", () => { + { + const { info, error, isLoading } = suspense.useRoomInfo("room-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.url).toEqualTypeOf(); + expectTypeOf(info?.type).toEqualTypeOf<"public" | "private">(); + // @ts-expect-error + void info?.nonexisting; + expectTypeOf(error).toEqualTypeOf(); + } + }); -// The useDeleteComment() hook -{ - const deleteComment = classic.useDeleteComment(); + test("useGroupInfo()", () => { + { + const { info, error, isLoading } = classic.useGroupInfo("group-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info!.name).toEqualTypeOf(); + expectTypeOf(info!.avatar).toEqualTypeOf(); + expectTypeOf(info?.type).toEqualTypeOf<"open" | "closed" | undefined>(); + // @ts-expect-error + void info?.nonexisting; + expectTypeOf(error).toEqualTypeOf(); + } + }); - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); + test("useGroupInfo() (suspense)", () => { + { + const { info, error, isLoading } = suspense.useGroupInfo("group-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.avatar).toEqualTypeOf(); + expectTypeOf(info?.type).toEqualTypeOf<"open" | "closed">(); + // @ts-expect-error + void info?.nonexisting; + expectTypeOf(error).toEqualTypeOf(); + } + }); - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} + test("useCreateThread()", () => { + { + const createThread = classic.useCreateThread(); + // @ts-expect-error + void createThread({}); // no body = error + + // No metadata = error + void ( + // @ts-expect-error + createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }) + ); + + const thread = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { color: "red" }, + commentMetadata: { priority: 1 }, + }); + + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void thread.metadata.nonexisting; + } + }); -// The useDeleteComment() hook (suspense) -{ - const deleteComment = suspense.useDeleteComment(); + test("useCreateThread() (suspense)", () => { + { + const createThread = suspense.useCreateThread(); + // @ts-expect-error + void createThread({}); // no body = error + + // No metadata = error + void ( + // @ts-expect-error + createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }) + ); + + const thread = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { color: "red" }, + commentMetadata: { priority: 1 }, + }); + + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void thread.metadata.nonexisting; + } + }); - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); + test("useEditThreadMetadata()", () => { + { + const editMetadata = classic.useEditThreadMetadata(); + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + metadata: { color: "red", pinned: null }, + }) + ).toEqualTypeOf(); + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { color: null }, + }); // Color isn't optional, so cannot be wiped + } + }); - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} + test("useEditThreadMetadata() (suspense)", () => { + { + const editMetadata = suspense.useEditThreadMetadata(); + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + metadata: { color: "red", pinned: null }, + }) + ).toEqualTypeOf(); + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { color: null }, + }); // Color isn't optional, so cannot be wiped + } + }); -// --------------------------------------------------------- + test("useCreateComment()", () => { + { + const createComment = classic.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { + priority: 2, + reviewed: false, + }, + }); + + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.metadata.priority).toEqualTypeOf(); + expectTypeOf(comment.metadata.reviewed).toEqualTypeOf< + boolean | undefined + >(); + // @ts-expect-error + void comment.metadata.nonexisting; + } + }); -// The useAddReaction() hook -{ - const addReaction = classic.useAddReaction(); + test("useCreateComment() (suspense)", () => { + { + const createComment = suspense.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { priority: 1 }, + }); + + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.metadata.priority).toEqualTypeOf(); + // @ts-expect-error + void comment.metadata.nonexisting; + } + }); - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); + test("useEditComment()", () => { + { + const editComment = classic.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2, reviewed: null }, + }) + ).toEqualTypeOf(); + + void editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + // @ts-expect-error + metadata: { nonexisting: null }, + }); + } + }); - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} + test("useEditComment() (suspense)", () => { + { + const editComment = suspense.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2 }, + }) + ).toEqualTypeOf(); + + void editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + // @ts-expect-error + metadata: { nonexisting: null }, + }); + } + }); -// The useAddReaction() hook (suspense) -{ - const addReaction = suspense.useAddReaction(); - - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} + test("useEditCommentMetadata()", () => { + { + const editMetadata = classic.useEditCommentMetadata(); + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { priority: 2, reviewed: null }, + }) + ).toEqualTypeOf(); + + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { priority: null }, + }); // priority isn't optional, so it cannot be wiped + } + }); -// --------------------------------------------------------- + test("useEditCommentMetadata() (suspense)", () => { + { + const editMetadata = suspense.useEditCommentMetadata(); + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { priority: 2, reviewed: null }, + }) + ).toEqualTypeOf(); + + void editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + // @ts-expect-error + metadata: { priority: null }, + }); // priority isn't optional, so it cannot be wiped + } + }); -// The useRemoveReaction() hook -{ - const removeReaction = classic.useRemoveReaction(); + test("useDeleteComment()", () => { + { + const deleteComment = classic.useDeleteComment(); - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); + } + }); -// The useRemoveReaction() hook (suspense) -{ - const removeReaction = suspense.useRemoveReaction(); - - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} + test("useDeleteComment() (suspense)", () => { + { + const deleteComment = suspense.useDeleteComment(); -// --------------------------------------------------------- - -// The useInboxNotifications() hook -{ - expectType(classic.useInboxNotifications().isLoading); - expectType(classic.useInboxNotifications().error); - expectType<("thread" | "textMention" | `$${string}`)[] | undefined>( - classic.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[] | undefined>( - classic.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.roomId) - ); -} + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); -// The useInboxNotifications() hook (suspense) -{ - expectType(suspense.useInboxNotifications().isLoading); - expectType(suspense.useInboxNotifications().error); - expectType<("thread" | "textMention" | `$${string}`)[]>( - suspense.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[]>( - suspense - .useInboxNotifications() - .inboxNotifications?.map((ibn) => ibn.roomId) - ); -} + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useAddReaction()", () => { + { + const addReaction = classic.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); -// The useInboxNotificationThread() hook -{ - const result = classic.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType<"red" | "blue">(result.metadata.color); - expectError(result.metadata.nonexisting); -} + test("useAddReaction() (suspense)", () => { + { + const addReaction = suspense.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); -// The useInboxNotificationThread() hook (suspense) -{ - const result = suspense.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType<"red" | "blue">(result.metadata.color); - expectError(result.metadata.nonexisting); -} + test("useRemoveReaction()", () => { + { + const removeReaction = classic.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useRemoveReaction() (suspense)", () => { + { + const removeReaction = suspense.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); -// The useMarkInboxNotificationAsRead() hook -{ - const markRead = classic.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} + test("useInboxNotifications()", () => { + { + expectTypeOf( + classic.useInboxNotifications().isLoading + ).toEqualTypeOf(); + expectTypeOf(classic.useInboxNotifications().error).toEqualTypeOf< + Error | undefined + >(); + expectTypeOf( + classic + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf< + ("thread" | "textMention" | `$${string}`)[] | undefined + >(); + expectTypeOf( + classic + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[] | undefined>(); + } + }); -// The useMarkInboxNotificationAsRead() hook (suspense) -{ - const markRead = suspense.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} + test("useInboxNotifications() (suspense)", () => { + { + expectTypeOf( + suspense.useInboxNotifications().isLoading + ).toEqualTypeOf(); + expectTypeOf( + suspense.useInboxNotifications().error + ).toEqualTypeOf(); + expectTypeOf( + suspense + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf<("thread" | "textMention" | `$${string}`)[]>(); + expectTypeOf( + suspense + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[]>(); + } + }); -// --------------------------------------------------------- + test("useInboxNotificationThread()", () => { + { + const result = classic.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void result.metadata.nonexisting; + } + }); -// The useMarkAllInboxNotificationsAsRead() hook -{ - const markAllRead = classic.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} + test("useInboxNotificationThread() (suspense)", () => { + { + const result = suspense.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void result.metadata.nonexisting; + } + }); -// The useMarkAllInboxNotificationsAsRead() hook (suspense) -{ - const markAllRead = suspense.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} + test("useMarkInboxNotificationAsRead()", () => { + { + const markRead = classic.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useMarkInboxNotificationAsRead() (suspense)", () => { + { + const markRead = suspense.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); -// The useDeleteInboxNotification() hook -{ - const deleteNotification = classic.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} + test("useMarkAllInboxNotificationsAsRead()", () => { + { + const markAllRead = classic.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); -// The useDeleteInboxNotification() hook (suspense) -{ - const deleteNotification = suspense.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} + test("useMarkAllInboxNotificationsAsRead() (suspense)", () => { + { + const markAllRead = suspense.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useDeleteInboxNotification()", () => { + { + const deleteNotification = classic.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); -// The useDeleteAllInboxNotifications() hook -{ - const deleteAllNotifications = classic.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} + test("useDeleteInboxNotification() (suspense)", () => { + { + const deleteNotification = suspense.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); -// The useDeleteAllInboxNotifications() hook (suspense) -{ - const deleteAllNotifications = suspense.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} + test("useDeleteAllInboxNotifications()", () => { + { + const deleteAllNotifications = classic.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useDeleteAllInboxNotifications() (suspense)", () => { + { + const deleteAllNotifications = suspense.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); -// The useUnreadInboxNotificationsCount() hook -{ - const { count, error, isLoading } = - classic.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} + test("useUnreadInboxNotificationsCount()", () => { + { + const { count, error, isLoading } = + classic.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); -// The useUnreadInboxNotificationsCount() hook (suspense) -{ - const { count, error, isLoading } = - suspense.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} + test("useUnreadInboxNotificationsCount() (suspense)", () => { + { + const { count, error, isLoading } = + suspense.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); -// --------------------------------------------------------- + test("useSyncStatus()", () => { + { + const status = classic.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + { + const status = suspense.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + }); -// The useSyncStatus() hook -{ - const status = classic.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} -{ - const status = suspense.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} + test("useNotificationSettings()", () => { + { + const [{ isLoading, error, settings }, update] = + classic.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); -// --------------------------------------------------------- -// the useNotificationSettings() hook -{ - const [{ isLoading, error, settings }, update] = - classic.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// the useNotificationSettings() hook suspense -{ - const [{ isLoading, error, settings }, update] = - suspense.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// --------------------------------------------------------- - -// The useAiChatStatus() hook -{ - const status = classic.useAiChatStatus("chat-id"); - expectType<"disconnected" | "idle" | "loading" | "generating">(status.status); - if (status.status === "generating") { - // The partType might not exist if there's no content yet - expectType< - | "text" - | "reasoning" - | "retrieval" - | "tool-invocation" - | "sources" - | undefined - >(status.partType); - if (status.partType === "tool-invocation") { - expectType(status.toolName); - } else { - expectType(status.toolName); - } - } else { - expectType(status.partType); - expectType(status.toolName); - } -} + test("useNotificationSettings() (suspense)", () => { + { + const [{ isLoading, error, settings }, update] = + suspense.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); -// The useAiChatStatus() hook (suspense) -{ - const status = suspense.useAiChatStatus("chat-id"); - expectType<"disconnected" | "idle" | "loading" | "generating">(status.status); - if (status.status === "generating") { - // The partType might not exist if there's no content yet - expectType< - | "text" - | "reasoning" - | "retrieval" - | "tool-invocation" - | "sources" - | undefined - >(status.partType); - if (status.partType === "tool-invocation") { - expectType(status.toolName); - } else { - expectType(status.toolName); - } - } else { - expectType(status.partType); - expectType(status.toolName); - } -} + test("useAiChatStatus()", () => { + { + const status = classic.useAiChatStatus("chat-id"); + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "idle" | "loading" | "generating" + >(); + if (status.status === "generating") { + // The partType might not exist if there's no content yet + expectTypeOf(status.partType).toEqualTypeOf< + | "text" + | "reasoning" + | "retrieval" + | "tool-invocation" + | "sources" + | undefined + >(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); -// The useAiChatStatus() hook with optional branchId -{ - const status = classic.useAiChatStatus("chat-id", "ms_branch" as any); - if (status.status === "generating") { - // The partType might not exist if there's no content yet - expectType< - | "text" - | "reasoning" - | "retrieval" - | "tool-invocation" - | "sources" - | undefined - >(status.partType); - if (status.partType === "tool-invocation") { - expectType(status.toolName); - } else { - expectType(status.toolName); - } - } else { - expectType(status.partType); - expectType(status.toolName); - } -} + test("useAiChatStatus() (suspense)", () => { + { + const status = suspense.useAiChatStatus("chat-id"); + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "idle" | "loading" | "generating" + >(); + if (status.status === "generating") { + // The partType might not exist if there's no content yet + expectTypeOf(status.partType).toEqualTypeOf< + | "text" + | "reasoning" + | "retrieval" + | "tool-invocation" + | "sources" + | undefined + >(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); -// The useAiChatStatus() hook with optional branchId (suspense) -{ - const status = suspense.useAiChatStatus("chat-id", "ms_branch" as any); - if (status.status === "generating") { - // The partType might not exist if there's no content yet - expectType< - | "text" - | "reasoning" - | "retrieval" - | "tool-invocation" - | "sources" - | undefined - >(status.partType); - if (status.partType === "tool-invocation") { - expectType(status.toolName); - } else { - expectType(status.toolName); - } - } else { - expectType(status.partType); - expectType(status.toolName); - } -} + test("useAiChatStatus() with optional branchId", () => { + { + const status = classic.useAiChatStatus("chat-id", "ms_branch" as any); + if (status.status === "generating") { + // The partType might not exist if there's no content yet + expectTypeOf(status.partType).toEqualTypeOf< + | "text" + | "reasoning" + | "retrieval" + | "tool-invocation" + | "sources" + | undefined + >(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); -// --------------------------------------------------------- + test("useAiChatStatus() with optional branchId (suspense)", () => { + { + const status = suspense.useAiChatStatus("chat-id", "ms_branch" as any); + if (status.status === "generating") { + // The partType might not exist if there's no content yet + expectTypeOf(status.partType).toEqualTypeOf< + | "text" + | "reasoning" + | "retrieval" + | "tool-invocation" + | "sources" + | undefined + >(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); +}); diff --git a/packages/liveblocks-react/test-d/factories.test-d.tsx b/packages/liveblocks-react/test-d/factories.test-d.tsx index 3c4af02ed68..802bb6df54c 100644 --- a/packages/liveblocks-react/test-d/factories.test-d.tsx +++ b/packages/liveblocks-react/test-d/factories.test-d.tsx @@ -1,3 +1,4 @@ +import React from "react"; import type { NotificationSettings, Json, @@ -11,7 +12,7 @@ import { createRoomContext, type AiChatStatus, } from "@liveblocks/react"; -import { expectAssignable, expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; type MyPresence = { cursor: { x: number; y: number }; @@ -51,1324 +52,1565 @@ const client = createClient({ publicApiKey: "pk_whatever" }); const lbctx = createLiveblocksContext(client); const ctx = createRoomContext(client); -// LiveblocksProvider -{ - const LiveblocksProvider = lbctx.LiveblocksProvider; - - // The only valid instantiation of the as returned from - // a context factory is one without any props! This is because the factory - // itself already binds it to the client instance. - ; - - // So all of the following ones are errors - expectError(); - expectError(); - expectError(); - expectError(); - expectError( - ({ token: "token" })} - throttle={16} - /> - ); - expectError( - [{ foo: "bar" }]} - /> - ); - expectError( - [{ name: "Vincent", age: 42 }]} - /> - ); -} - -// LiveblocksProvider (suspense) -{ - const LiveblocksProvider = lbctx.suspense.LiveblocksProvider; - - // The only valid instantiation of the as returned from - // a context factory is one without any props! This is because the factory - // itself already binds it to the client instance. - ; - - // So all of the following ones are errors - expectError(); - expectError(); - expectError(); - expectError(); - expectError( - ({ token: "token" })} - throttle={16} - /> - ); - expectError( - [{ foo: "bar" }]} - /> - ); - expectError( - [{ name: "Vincent", age: 42 }]} - /> - ); -} - -// RoomProvider -{ - const RoomProvider = ctx.RoomProvider; - - // Missing mandatory props is an error - expectError( - -
- - ); - - expectError( - // Missing mandatory initialPresence + initialStorage - -
- - ); - - expectError( - // Missing mandatory initialStorage - -
- - ); - - expectError( - // Missing mandatory initialPresence - -
- - ); - - expectError( - // Incorrect initialStorage - -
- - ); - - -
- ; -} - -// RoomProvider (suspense) -{ - const RoomProvider = ctx.suspense.RoomProvider; - - // Missing mandatory props is an error - expectError( - -
- - ); - - expectError( - -
- - ); - - expectError( - // Missing mandatory initialStorage - -
- - ); - - expectError( - // Missing mandatory initialPresence - -
- - ); - - expectError( - // Incorrect initialStorage - -
- - ); - - -
- ; -} - -// --------------------------------------------------------- -// Hook APIs -// --------------------------------------------------------- - -// The useRoom() hook -expectType>(ctx.useRoom()); -expectType>(ctx.useRoom({ allowOutsideRoom: false })); -expectType | null>( - ctx.useRoom({ allowOutsideRoom: Math.random() < 0.5 }) -); -expectType | null>(ctx.useRoom({ allowOutsideRoom: true })); - -// useIsInsideRoom() -expectType(ctx.useIsInsideRoom()); -expectType(lbctx.useIsInsideRoom()); - -// The presence hooks -expectType

(ctx.useSelf()!.presence); -expectType[]>(ctx.useOthers()); -expectType

(ctx.useOthers()[0]!.presence); -expectType

(ctx.useOthersMapped((u) => u.presence)[0]![1]); -expectType(ctx.useOthersConnectionIds()); -expectType

(ctx.useOther(123, (o) => o.presence)); -expectType

(ctx.useMyPresence()[0]); - -// The presence hooks (suspense versions) -expectType

(ctx.suspense.useSelf().presence); -expectType[]>(ctx.suspense.useOthers()); -expectType

(ctx.suspense.useOthers()[0]!.presence); -expectType

(ctx.suspense.useOthersMapped((u) => u.presence)[0]![1]); -expectType(ctx.suspense.useOthersConnectionIds()); -expectType

(ctx.suspense.useOther(123, (o) => o.presence)); -expectType

(ctx.suspense.useMyPresence()[0]); - -// The storage hooks -expectType(ctx.useStorage((x) => x.animals)); -expectType<{ readonly [key: string]: number } | null>( - ctx.useStorage((x) => x.scores) -); -expectType<{ readonly name: string; readonly age: number } | null>( - ctx.useStorage((x) => x.person) -); - -expectType<[root: LiveObject | null]>(ctx.useStorageRoot()); - -// The storage hooks (suspense versions) -expectType(ctx.suspense.useStorage((x) => x.animals)); -expectType<{ readonly [key: string]: number }>( - ctx.suspense.useStorage((x) => x.scores) -); -expectType<{ readonly name: string; readonly age: number }>( - ctx.suspense.useStorage((x) => x.person) -); - -expectType<[root: LiveObject | null]>(ctx.suspense.useStorageRoot()); -// ^^^^ Despite being a Suspense hook, -// this one still returns `null`, -// as it's used as a building -// block. This is NOT a bug. - -// The useOthersListener() hook -ctx.useOthersListener((event) => { - expectType[]>(event.others); - switch (event.type) { - case "enter": - expectType>(event.user); - return; - case "leave": - expectType>(event.user); - return; - case "update": - expectType>(event.user); - expectType>(event.updates); - return; - case "reset": - // No extra fields on reset - return; - default: - expectType(event); - } -}); - -// The useOthersListener() hook with inline unpacking -ctx.useOthersListener(({ user, type }) => { - expectType | undefined>(user); - expectType<"enter" | "leave" | "update" | "reset">(type); - switch (type) { - case "enter": - expectType>(user); - return; - case "leave": - expectType>(user); - return; - case "update": - expectType>(user); - return; - case "reset": - // No extra fields on reset - expectType(user); - return; - default: - expectType(type); - } -}); +describe("createLiveblocksContext / createRoomContext factories", () => { + test("LiveblocksProvider", () => { + { + const LiveblocksProvider = lbctx.LiveblocksProvider; + + // The only valid instantiation of the as returned from + // a context factory is one without any props! This is because the factory + // itself already binds it to the client instance. + ; + + // So all of the following ones are errors + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ({ token: "token" })} + throttle={16} + /> + ); + void ( + [{ foo: "bar" }]} + /> + ); + void ( + [{ name: "Vincent", age: 42 }]} + /> + ); + } + }); -// useErrorListener() -{ - ctx.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... + test("LiveblocksProvider (suspense)", () => { + { + const LiveblocksProvider = lbctx.suspense.LiveblocksProvider; + + // The only valid instantiation of the as returned from + // a context factory is one without any props! This is because the factory + // itself already binds it to the client instance. + ; + + // So all of the following ones are errors + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ({ token: "token" })} + throttle={16} + /> + ); + void ( + [{ foo: "bar" }]} + /> + ); + void ( + [{ name: "Vincent", age: 42 }]} + /> + ); } }); - lbctx.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... + test("RoomProvider", () => { + { + const RoomProvider = ctx.RoomProvider; + + // Missing mandatory props is an error + void ( + ( + // @ts-expect-error + +

+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + +
+ + ) + ); + + void ( + +
+ + ); + + +
+ ; } }); - lbctx.suspense.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... + test("RoomProvider (suspense)", () => { + { + const RoomProvider = ctx.suspense.RoomProvider; + + // Missing mandatory props is an error + void ( + ( + // @ts-expect-error + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialStorage + +
+ + ) + ); + + void ( + ( + // @ts-expect-error - Missing mandatory initialPresence + +
+ + ) + ); + + void ( + +
+ + ); + + +
+ ; } }); -} - -// --------------------------------------------------------- - -// useSelf() -{ - const me = ctx.useSelf(); - expectType(me?.presence.cursor.x); - expectError(me?.presence.nonexisting); - - expectType(me?.info.name); - expectType(me?.info.age); - expectError(me?.info.nonexisting); -} - -// useSelf() (suspense) -{ - const me = ctx.suspense.useSelf(); - expectType(me.presence.cursor.x); - expectError(me.presence.nonexisting); - - expectType(me.info.name); - expectType(me.info.age); - expectError(me.info.nonexisting); -} - -// --------------------------------------------------------- - -// The useMutation() hook -{ - expectType<(a: number, b: boolean) => "hi">( - ctx.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor.x); - expectError(mut.self.presence.nonexisting); - expectType(mut.self.info.name); - expectType(mut.self.info.age); - expectError(mut.self.info.nonexisting); - - expectType(mut.others[0]!.presence.cursor.x); - expectError(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info.name); - expectType(mut.others[0]!.info.age); - expectError(mut.others[0]!.info.nonexisting); - - expectType(mut.storage.get("animals").get(0)); - expectType(mut.storage.get("scores").get("one")); - expectType(mut.storage.get("person").get("age")); - expectError(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectError(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} - -// The useMutation() hook (suspense) -{ - expectType<(a: number, b: boolean) => "hi">( - ctx.suspense.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor.x); - expectError(mut.self.presence.nonexisting); - expectType(mut.self.info.name); - expectType(mut.self.info.age); - expectError(mut.self.info.nonexisting); - - expectType(mut.others[0]!.presence.cursor.x); - expectError(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info.name); - expectType(mut.others[0]!.info.age); - expectError(mut.others[0]!.info.nonexisting); - - expectType(mut.storage.get("animals").get(0)); - expectType(mut.storage.get("scores").get("one")); - expectType(mut.storage.get("person").get("age")); - expectError(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectError(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} - -// --------------------------------------------------------- - -// The useUser() hook -{ - { - const { user, error, isLoading } = ctx.useUser("user-id"); - // ^^^ [1] - expectType(isLoading); - expectType(user?.name); - expectType(user?.age); - expectType(error); - } - { - const { user, error, isLoading } = lbctx.useUser("user-id"); - // ^^^^^ [2] - expectType(isLoading); - expectType(user?.name); - expectType(user?.age); - expectType(error); - } -} - -// The useUser() hook (suspense) -{ - { - const { user, error, isLoading } = ctx.suspense.useUser("user-id"); - // ^^^^^^^^^^^^ [3] - expectType(isLoading); - expectType(user.name); - expectType(user.age); - expectType(error); - } - { - const { user, error, isLoading } = lbctx.suspense.useUser("user-id"); - // ^^^^^^^^^^^^^^ [4] - expectType(isLoading); - expectType(user.name); - expectType(user.age); - expectType(error); - } -} - -// --------------------------------------------------------- - -// The useRoomInfo() hook -{ - { - const { info, error, isLoading } = ctx.useRoomInfo("room-id"); - // ^^^ [1] - expectType(isLoading); - expectType(info?.name); - expectType(info?.url); - expectType(info?.nonexisting); - expectType(error); - } - { - const { info, error, isLoading } = lbctx.useRoomInfo("room-id"); - // ^^^^^ [2] - expectType(isLoading); - expectType(info?.name); - expectType(info?.url); - expectType(info?.nonexisting); - expectType(error); - } -} - -// The useRoomInfo() hook (suspense) -{ - { - const { info, error, isLoading } = ctx.suspense.useRoomInfo("room-id"); - // ^^^^^^^^^^^^ [3] - expectType(isLoading); - expectType(info.name); - expectType(info.url); - expectType(info.nonexisting); - expectType(error); - } - { - const { info, error, isLoading } = lbctx.suspense.useRoomInfo("room-id"); - // ^^^^^^^^^^^^^^ [4] - expectType(isLoading); - expectType(info.name); - expectType(info.url); - expectType(info.nonexisting); - expectType(error); - } -} - -// --------------------------------------------------------- - -// The useGroupInfo() hook -{ - { - const { info, error, isLoading } = ctx.useGroupInfo("group-id"); - // ^^^ [1] - expectType(isLoading); - expectType(info?.name); - expectType(info?.avatar); - expectType(info?.nonexisting); - expectType(error); - } - { - const { info, error, isLoading } = lbctx.useGroupInfo("group-id"); - // ^^^^^ [2] - expectType(isLoading); - expectType(info?.name); - expectType(info?.avatar); - expectType(info?.nonexisting); - expectType(error); - } -} - -// The useGroupInfo() hook (suspense) -{ - { - const { info, error, isLoading } = ctx.suspense.useGroupInfo("group-id"); - // ^^^^^^^^^^^^ [3] - expectType(isLoading); - expectType(info.name); - expectType(info.avatar); - expectType(info.nonexisting); - expectType(error); - } - { - const { info, error, isLoading } = lbctx.suspense.useGroupInfo("group-id"); - // ^^^^^^^^^^^^^^ [4] - expectType(isLoading); - expectType(info.name); - expectType(info.avatar); - expectType(info.nonexisting); - expectType(error); - } -} - -// --------------------------------------------------------- - -// The useCreateThread() hook -{ - { - const untypedCtx = createRoomContext(client); - const createThread = untypedCtx.useCreateThread(); - // ^^^^^^^^^^ [1] - expectError(createThread({})); - - const thread1 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); - expectType<"thread">(thread1.type); - expectType(thread1.id); - expectType(thread1.roomId); - expectType<"comment">(thread1.comments[0]!.type); - expectType(thread1.comments[0]!.id); - expectType(thread1.comments[0]!.threadId); - - expectType(thread1.metadata.color); - - // But... creating a thread _with_ metadata is now an error - const thread2 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { foo: "bar" }, - }); + test("useRoom()", () => { + expectTypeOf(ctx.useRoom()).toEqualTypeOf>(); + expectTypeOf(ctx.useRoom({ allowOutsideRoom: false })).toEqualTypeOf< + Room + >(); + expectTypeOf( + ctx.useRoom({ allowOutsideRoom: Math.random() < 0.5 }) + ).toEqualTypeOf | null>(); + expectTypeOf(ctx.useRoom({ allowOutsideRoom: true })).toEqualTypeOf | null>(); + }); - expectType(thread2.id); - } + test("useIsInsideRoom()", () => { + expectTypeOf(ctx.useIsInsideRoom).parameters.toEqualTypeOf<[]>(); + expectTypeOf(ctx.useIsInsideRoom).returns.toEqualTypeOf(); + expectTypeOf(ctx.useIsInsideRoom()).toEqualTypeOf(); + expectTypeOf(lbctx.useIsInsideRoom).parameters.toEqualTypeOf<[]>(); + expectTypeOf(lbctx.useIsInsideRoom).returns.toEqualTypeOf(); + expectTypeOf(lbctx.useIsInsideRoom()).toEqualTypeOf(); + }); - { - const createThread = ctx.useCreateThread(); - // ^^^ [2] - expectError(createThread({})); // no body = error + test("presence hooks", () => { + expectTypeOf(ctx.useSelf()!.presence).toEqualTypeOf

(); + expectTypeOf(ctx.useOthers()).toEqualTypeOf[]>(); + expectTypeOf(ctx.useOthers()[0]!.presence).toEqualTypeOf

(); + expectTypeOf( + ctx.useOthersMapped((u) => u.presence)[0]![1] + ).toEqualTypeOf

(); + expectTypeOf(ctx.useOthersConnectionIds()).toEqualTypeOf< + readonly number[] + >(); + expectTypeOf(ctx.useOther(123, (o) => o.presence)).toEqualTypeOf

(); + expectTypeOf(ctx.useMyPresence()[0]).toEqualTypeOf

(); + }); - // No metadata = error - expectError( - createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }) - ); - - const thread = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { color: "red" }, - }); + test("presence hooks (suspense)", () => { + expectTypeOf(ctx.suspense.useSelf().presence).toEqualTypeOf

(); + expectTypeOf(ctx.suspense.useOthers()).toEqualTypeOf< + readonly User[] + >(); + expectTypeOf(ctx.suspense.useOthers()[0]!.presence).toEqualTypeOf

(); + expectTypeOf( + ctx.suspense.useOthersMapped((u) => u.presence)[0]![1] + ).toEqualTypeOf

(); + expectTypeOf(ctx.suspense.useOthersConnectionIds()).toEqualTypeOf< + readonly number[] + >(); + expectTypeOf( + ctx.suspense.useOther(123, (o) => o.presence) + ).toEqualTypeOf

(); + expectTypeOf(ctx.suspense.useMyPresence()[0]).toEqualTypeOf

(); + }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType<"comment">(thread.comments[0]!.type); - expectType(thread.comments[0]!.id); - expectType(thread.comments[0]!.threadId); - - expectType<"red" | "blue">(thread.metadata.color); - } -} - -// The useCreateThread() hook (suspense) -{ - { - const untypedCtx = createRoomContext(client); - const createThread = untypedCtx.suspense.useCreateThread(); - // ^^^^^^^^^^^^^^^^^^^ [3] - expectError(createThread({})); - - const thread1 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, + test("storage hooks", () => { + expectTypeOf(ctx.useStorage((x) => x.animals)).toEqualTypeOf< + readonly string[] | null + >(); + expectTypeOf(ctx.useStorage((x) => x.scores)).toEqualTypeOf<{ + readonly [key: string]: number; + } | null>(); + expectTypeOf(ctx.useStorage((x) => x.person)).toEqualTypeOf<{ + readonly name: string; + readonly age: number; + } | null>(); + + expectTypeOf(ctx.useStorageRoot()).toEqualTypeOf< + [root: LiveObject | null] + >(); + }); + + test("storage hooks (suspense)", () => { + expectTypeOf(ctx.suspense.useStorage((x) => x.animals)).toEqualTypeOf< + readonly string[] + >(); + expectTypeOf(ctx.suspense.useStorage((x) => x.scores)).toEqualTypeOf<{ + readonly [key: string]: number; + }>(); + expectTypeOf(ctx.suspense.useStorage((x) => x.person)).toEqualTypeOf<{ + readonly name: string; + readonly age: number; + }>(); + + expectTypeOf(ctx.suspense.useStorageRoot()).toEqualTypeOf< + [root: LiveObject | null] + >(); + // ^^^^ Despite being a Suspense hook, + // this one still returns `null`, + // as it's used as a building + // block. This is NOT a bug. + }); + + test("useOthersListener()", () => { + ctx.useOthersListener((event) => { + expectTypeOf(event.others).toEqualTypeOf[]>(); + switch (event.type) { + case "enter": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "leave": + expectTypeOf(event.user).toEqualTypeOf>(); + return; + case "update": + expectTypeOf(event.user).toEqualTypeOf>(); + expectTypeOf(event.updates).toEqualTypeOf>(); + return; + case "reset": + // No extra fields on reset + return; + default: + expectTypeOf(event).toEqualTypeOf(); + } }); + }); - expectType<"thread">(thread1.type); - expectType(thread1.id); - expectType(thread1.roomId); - expectType<"comment">(thread1.comments[0]!.type); - expectType(thread1.comments[0]!.id); - expectType(thread1.comments[0]!.threadId); - - expectType(thread1.metadata.color); - - const thread2 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { foo: "bar" }, + test("useOthersListener() with inline unpacking", () => { + ctx.useOthersListener(({ user, type }) => { + expectTypeOf(user).toEqualTypeOf | undefined>(); + expectTypeOf(type).toEqualTypeOf< + "enter" | "leave" | "update" | "reset" + >(); + switch (type) { + case "enter": + expectTypeOf(user).toEqualTypeOf>(); + return; + case "leave": + expectTypeOf(user).toEqualTypeOf>(); + return; + case "update": + expectTypeOf(user).toEqualTypeOf>(); + return; + case "reset": + // No extra fields on reset + expectTypeOf(user).toEqualTypeOf(); + return; + default: + expectTypeOf(type).toEqualTypeOf(); + } }); + }); + + test("useErrorListener()", () => { + { + ctx.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + + lbctx.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + + lbctx.suspense.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + } + }); + + test("useSelf()", () => { + { + const me = ctx.useSelf(); + expectTypeOf(me?.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void me?.presence.nonexisting; + + expectTypeOf(me?.info.name).toEqualTypeOf(); + expectTypeOf(me?.info.age).toEqualTypeOf(); + // @ts-expect-error + void me?.info.nonexisting; + } + }); + + test("useSelf() (suspense)", () => { + { + const me = ctx.suspense.useSelf(); + expectTypeOf(me.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void me.presence.nonexisting; + + expectTypeOf(me.info.name).toEqualTypeOf(); + expectTypeOf(me.info.age).toEqualTypeOf(); + // @ts-expect-error + void me.info.nonexisting; + } + }); + + test("useMutation()", () => { + { + expectTypeOf( + ctx.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void mut.self.presence.nonexisting; + expectTypeOf(mut.self.info.name).toEqualTypeOf(); + expectTypeOf(mut.self.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.self.info.nonexisting; + + expectTypeOf( + mut.others[0]!.presence.cursor.x + ).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.presence.nonexisting; + expectTypeOf(mut.others[0]!.info.name).toEqualTypeOf(); + expectTypeOf(mut.others[0]!.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.info.nonexisting; + + expectTypeOf(mut.storage.get("animals").get(0)).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.storage.get("scores").get("one")).toEqualTypeOf< + number | undefined + >(); + expectTypeOf( + mut.storage.get("person").get("age") + ).toEqualTypeOf(); + // @ts-expect-error + void mut.storage.get("nonexisting"); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + // @ts-expect-error + void mut.setMyPresence({ nonexisting: 123 }); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } + }); + + test("useMutation() (suspense)", () => { + { + expectTypeOf( + ctx.suspense.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor.x).toEqualTypeOf(); + // @ts-expect-error + void mut.self.presence.nonexisting; + expectTypeOf(mut.self.info.name).toEqualTypeOf(); + expectTypeOf(mut.self.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.self.info.nonexisting; + + expectTypeOf( + mut.others[0]!.presence.cursor.x + ).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.presence.nonexisting; + expectTypeOf(mut.others[0]!.info.name).toEqualTypeOf(); + expectTypeOf(mut.others[0]!.info.age).toEqualTypeOf(); + // @ts-expect-error + void mut.others[0]!.info.nonexisting; + + expectTypeOf(mut.storage.get("animals").get(0)).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.storage.get("scores").get("one")).toEqualTypeOf< + number | undefined + >(); + expectTypeOf( + mut.storage.get("person").get("age") + ).toEqualTypeOf(); + // @ts-expect-error + void mut.storage.get("nonexisting"); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + // @ts-expect-error + void mut.setMyPresence({ nonexisting: 123 }); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } + }); + + test("useUser()", () => { + { + { + const { user, error, isLoading } = ctx.useUser("user-id"); + // ^^^ [1] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user?.name).toEqualTypeOf(); + expectTypeOf(user?.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { user, error, isLoading } = lbctx.useUser("user-id"); + // ^^^^^ [2] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user?.name).toEqualTypeOf(); + expectTypeOf(user?.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); + + test("useUser() (suspense)", () => { + { + { + const { user, error, isLoading } = ctx.suspense.useUser("user-id"); + // ^^^^^^^^^^^^ [3] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user.name).toEqualTypeOf(); + expectTypeOf(user.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { user, error, isLoading } = lbctx.suspense.useUser("user-id"); + // ^^^^^^^^^^^^^^ [4] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user.name).toEqualTypeOf(); + expectTypeOf(user.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); - expectType(thread2.id); - } + test("useRoomInfo()", () => { + { + { + const { info, error, isLoading } = ctx.useRoomInfo("room-id"); + // ^^^ [1] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.url).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { info, error, isLoading } = lbctx.useRoomInfo("room-id"); + // ^^^^^ [2] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.url).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); - { - const createThread = ctx.suspense.useCreateThread(); - // ^^^^^^^^^^^^ [4] - expectError(createThread({})); // no body = error + test("useRoomInfo() (suspense)", () => { + { + { + const { info, error, isLoading } = ctx.suspense.useRoomInfo("room-id"); + // ^^^^^^^^^^^^ [3] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.url).toEqualTypeOf(); + expectTypeOf(info.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { info, error, isLoading } = + lbctx.suspense.useRoomInfo("room-id"); + // ^^^^^^^^^^^^^^ [4] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.url).toEqualTypeOf(); + expectTypeOf(info.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); - // No metadata = error - expectError( - createThread({ + test("useGroupInfo()", () => { + { + { + const { info, error, isLoading } = ctx.useGroupInfo("group-id"); + // ^^^ [1] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.avatar).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { info, error, isLoading } = lbctx.useGroupInfo("group-id"); + // ^^^^^ [2] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.avatar).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); + + test("useGroupInfo() (suspense)", () => { + { + { + const { info, error, isLoading } = + ctx.suspense.useGroupInfo("group-id"); + // ^^^^^^^^^^^^ [3] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.avatar).toEqualTypeOf(); + expectTypeOf(info.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + { + const { info, error, isLoading } = + lbctx.suspense.useGroupInfo("group-id"); + // ^^^^^^^^^^^^^^ [4] + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.avatar).toEqualTypeOf(); + expectTypeOf(info.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + } + }); + + test("useCreateThread()", () => { + { + { + const untypedCtx = createRoomContext(client); + const createThread = untypedCtx.useCreateThread(); + // ^^^^^^^^^^ [1] + // @ts-expect-error + void createThread({}); + + const thread1 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(thread1.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread1.id).toEqualTypeOf(); + expectTypeOf(thread1.roomId).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread1.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread1.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + + // But... creating a thread _with_ metadata is now an error + const thread2 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { foo: "bar" }, + }); + + expectTypeOf(thread2.id).toEqualTypeOf(); + } + + { + const createThread = ctx.useCreateThread(); + // ^^^ [2] + // @ts-expect-error + void createThread({}); // no body = error + + // No metadata = error + void ( + // @ts-expect-error + createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }) + ); + + const thread = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { color: "red" }, + }); + + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + } + } + }); + + test("useCreateThread() (suspense)", () => { + { + { + const untypedCtx = createRoomContext(client); + const createThread = untypedCtx.suspense.useCreateThread(); + // ^^^^^^^^^^^^^^^^^^^ [3] + // @ts-expect-error + void createThread({}); + + const thread1 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(thread1.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread1.id).toEqualTypeOf(); + expectTypeOf(thread1.roomId).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread1.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread1.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + + const thread2 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { foo: "bar" }, + }); + + expectTypeOf(thread2.id).toEqualTypeOf(); + } + + { + const createThread = ctx.suspense.useCreateThread(); + // ^^^^^^^^^^^^ [4] + // @ts-expect-error + void createThread({}); // no body = error + + // No metadata = error + void ( + // @ts-expect-error + createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }) + ); + + const thread = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { color: "red" }, + }); + + expectTypeOf(thread.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread.id).toEqualTypeOf(); + expectTypeOf(thread.roomId).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread.metadata.color).toEqualTypeOf<"red" | "blue">(); + } + } + }); + + test("useEditThreadMetadata()", () => { + { + { + const untypedCtx = createRoomContext(client); + const editMetadata = untypedCtx.useEditThreadMetadata(); + // ^^^^^^^^^^ [1] + // @ts-expect-error + void editMetadata({}); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) + ).toEqualTypeOf(); + } + + { + const editMetadata = ctx.useEditThreadMetadata(); + // ^^^ [2] + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + metadata: { color: "red", pinned: null }, + }) + ).toEqualTypeOf(); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { color: null }, + }); // Color isn't optional so cannot be wiped + } + } + }); + + test("useEditThreadMetadata() (suspense)", () => { + { + { + const untypedCtx = createRoomContext(client); + const editMetadata = untypedCtx.suspense.useEditThreadMetadata(); + // ^^^^^^^^^^^^^^^^^^^ [3] + // @ts-expect-error + void editMetadata({}); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) + ).toEqualTypeOf(); + } + + { + const editMetadata = ctx.suspense.useEditThreadMetadata(); + // ^^^^^^^^^^^^ [4] + // @ts-expect-error + void editMetadata({}); // no body = error + + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: null }, + }); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { nonexisting: 123 }, + }); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + metadata: { color: "red", pinned: null }, + }) + ).toEqualTypeOf(); + void editMetadata({ + threadId: "th_xxx", + // @ts-expect-error + metadata: { color: null }, + }); // Color isn't optional so cannot be wiped + } + } + }); + + test("useCreateComment()", () => { + { + { + const createComment = ctx.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + } + } + }); + + test("useCreateComment() (suspense)", () => { + { + const createComment = ctx.suspense.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", body: { version: 1, content: [{ type: "paragraph", children: [{ text: "hi" }] }], }, - }) - ); - - const thread = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { color: "red" }, - }); + }); - expectType<"thread">(thread.type); - expectType(thread.id); - expectType(thread.roomId); - expectType<"comment">(thread.comments[0]!.type); - expectType(thread.comments[0]!.id); - expectType(thread.comments[0]!.threadId); - - expectType<"red" | "blue">(thread.metadata.color); - } -} - -// --------------------------------------------------------- - -// The useEditThreadMetadata() hook -{ - { - const untypedCtx = createRoomContext(client); - const editMetadata = untypedCtx.useEditThreadMetadata(); - // ^^^^^^^^^^ [1] - expectError(editMetadata({})); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - } - - { - const editMetadata = ctx.useEditThreadMetadata(); - // ^^^ [2] - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ - threadId: "th_xxx", - metadata: { color: "red", pinned: null }, - }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { color: null } }) // Color isn't optional so cannot be wiped - ); - } -} - -// The useEditThreadMetadata() hook (suspense) -{ - { - const untypedCtx = createRoomContext(client); - const editMetadata = untypedCtx.suspense.useEditThreadMetadata(); - // ^^^^^^^^^^^^^^^^^^^ [3] - expectError(editMetadata({})); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - } - - { - const editMetadata = ctx.suspense.useEditThreadMetadata(); - // ^^^^^^^^^^^^ [4] - expectError(editMetadata({})); // no body = error - - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ - threadId: "th_xxx", - metadata: { color: "red", pinned: null }, - }) - ); - expectError( - editMetadata({ threadId: "th_xxx", metadata: { color: null } }) // Color isn't optional so cannot be wiped - ); - } -} - -// --------------------------------------------------------- - -// The useCreateComment() hook -{ - { - const createComment = ctx.useCreateComment(); - expectError(createComment({})); - - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + } + }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - } -} - -// The useCreateComment() hook (suspense) -{ - const createComment = ctx.suspense.useCreateComment(); - expectError(createComment({})); - - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, + test("useEditComment()", () => { + { + const editComment = ctx.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2 }, + }) + ).toEqualTypeOf(); + } }); - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); -} - -// --------------------------------------------------------- - -// The useEditComment() hook -{ - const editComment = ctx.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2 }, - }) - ); -} - -// The useEditComment() hook (suspense) -{ - const editComment = ctx.suspense.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2 }, - }) - ); -} - -// --------------------------------------------------------- - -// The useDeleteComment() hook -{ - const deleteComment = ctx.useDeleteComment(); - - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); - - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} - -// The useDeleteComment() hook (suspense) -{ - const deleteComment = ctx.suspense.useDeleteComment(); - - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); - - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} - -// --------------------------------------------------------- - -// The useAddReaction() hook -{ - const addReaction = ctx.useAddReaction(); - - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// The useAddReaction() hook (suspense) -{ - const addReaction = ctx.suspense.useAddReaction(); - - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// --------------------------------------------------------- - -// The useRemoveReaction() hook -{ - const removeReaction = ctx.useRemoveReaction(); - - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// The useRemoveReaction() hook (suspense) -{ - const removeReaction = ctx.suspense.useRemoveReaction(); - - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// --------------------------------------------------------- - -// The useInboxNotifications() hook -{ - const result = lbctx.useInboxNotifications(); - expectType(result.isLoading); - expectType(result.error); - expectType<("thread" | "textMention" | `$${string}`)[] | undefined>( - result.inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[] | undefined>( - result.inboxNotifications?.map((ibn) => ibn.roomId) - ); -} - -// The useInboxNotifications() hook (suspense) -{ - const result = lbctx.suspense.useInboxNotifications(); - expectType(result.isLoading); - expectType(result.error); - expectType<("thread" | "textMention" | `$${string}`)[]>( - result.inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[]>( - result.inboxNotifications?.map((ibn) => ibn.roomId) - ); -} - -// --------------------------------------------------------- - -// The useInboxNotificationThread() hook -{ - const result = lbctx.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType<"red" | "blue">(result.metadata.color); - expectError(result.metadata.nonexisting); -} - -// The useInboxNotificationThread() hook (suspense) -{ - const result = lbctx.suspense.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType<"red" | "blue">(result.metadata.color); - expectError(result.metadata.nonexisting); -} - -// --------------------------------------------------------- - -// The useMarkInboxNotificationAsRead() hook -{ - const markRead = lbctx.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} - -// The useMarkInboxNotificationAsRead() hook (suspense) -{ - const markRead = lbctx.suspense.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} - -// --------------------------------------------------------- - -// The useMarkAllInboxNotificationsAsRead() hook -{ - const markAllRead = lbctx.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} - -// The useMarkAllInboxNotificationsAsRead() hook (suspense) -{ - const markAllRead = lbctx.suspense.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} - -// --------------------------------------------------------- - -// The useDeleteInboxNotification() hook -{ - const deleteNotification = lbctx.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} - -// The useDeleteInboxNotification() hook (suspense) -{ - const deleteNotification = lbctx.suspense.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} - -// --------------------------------------------------------- - -// The useDeleteAllInboxNotifications() hook -{ - const deleteAllNotifications = lbctx.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} - -// The useDeleteAllInboxNotifications() hook (suspense) -{ - const deleteAllNotifications = - lbctx.suspense.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} - -// --------------------------------------------------------- - -// The useUnreadInboxNotificationsCount() hook -{ - const { count, error, isLoading } = lbctx.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} - -// The useUnreadInboxNotificationsCount() hook (suspense) -{ - const { count, error, isLoading } = - lbctx.suspense.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} - -// --------------------------------------------------------- - -// The useSyncStatus() hook -{ - const status = lbctx.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} -{ - const status = lbctx.suspense.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} -{ - const status = ctx.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} -{ - const status = ctx.suspense.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} - -// --------------------------------------------------------- -// the useNotificationSettings() hook -{ - const [{ isLoading, error, settings }, update] = - lbctx.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// the useNotificationSettings() hook suspense -{ - const [{ isLoading, error, settings }, update] = - lbctx.suspense.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// --------------------------------------------------------- - -// The useAiChatStatus() hook -{ - const status = lbctx.useAiChatStatus("chat-123"); - expectType(status); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType - ); - expectType(status.toolName); + test("useEditComment() (suspense)", () => { + { + const editComment = ctx.suspense.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2 }, + }) + ).toEqualTypeOf(); } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook (suspense) -{ - const status = lbctx.suspense.useAiChatStatus("chat-123"); - expectType(status); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType - ); - expectType(status.toolName); + }); + + test("useDeleteComment()", () => { + { + const deleteComment = ctx.useDeleteComment(); + + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); + + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook with branchId parameter -{ - const status = lbctx.useAiChatStatus("chat-123", "ms_branch" as any); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType - ); - expectType(status.toolName); + }); + + test("useDeleteComment() (suspense)", () => { + { + const deleteComment = ctx.suspense.useDeleteComment(); + + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); + + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); + } + }); + + test("useAddReaction()", () => { + { + const addReaction = ctx.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useAddReaction() (suspense)", () => { + { + const addReaction = ctx.suspense.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useRemoveReaction()", () => { + { + const removeReaction = ctx.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useRemoveReaction() (suspense)", () => { + { + const removeReaction = ctx.suspense.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useInboxNotifications()", () => { + { + const result = lbctx.useInboxNotifications(); + expectTypeOf(result.isLoading).toEqualTypeOf(); + expectTypeOf(result.error).toEqualTypeOf(); + expectTypeOf( + result.inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf< + ("thread" | "textMention" | `$${string}`)[] | undefined + >(); + expectTypeOf( + result.inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[] | undefined>(); + } + }); + + test("useInboxNotifications() (suspense)", () => { + { + const result = lbctx.suspense.useInboxNotifications(); + expectTypeOf(result.isLoading).toEqualTypeOf(); + expectTypeOf(result.error).toEqualTypeOf(); + expectTypeOf( + result.inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf<("thread" | "textMention" | `$${string}`)[]>(); + expectTypeOf( + result.inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[]>(); + } + }); + + test("useInboxNotificationThread()", () => { + { + const result = lbctx.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void result.metadata.nonexisting; + } + }); + + test("useInboxNotificationThread() (suspense)", () => { + { + const result = lbctx.suspense.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata.color).toEqualTypeOf<"red" | "blue">(); + // @ts-expect-error + void result.metadata.nonexisting; + } + }); + + test("useMarkInboxNotificationAsRead()", () => { + { + const markRead = lbctx.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); + + test("useMarkInboxNotificationAsRead() (suspense)", () => { + { + const markRead = lbctx.suspense.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); + + test("useMarkAllInboxNotificationsAsRead()", () => { + { + const markAllRead = lbctx.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); + + test("useMarkAllInboxNotificationsAsRead() (suspense)", () => { + { + const markAllRead = lbctx.suspense.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); + + test("useDeleteInboxNotification()", () => { + { + const deleteNotification = lbctx.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); + + test("useDeleteInboxNotification() (suspense)", () => { + { + const deleteNotification = lbctx.suspense.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); + + test("useDeleteAllInboxNotifications()", () => { + { + const deleteAllNotifications = lbctx.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); + + test("useDeleteAllInboxNotifications() (suspense)", () => { + { + const deleteAllNotifications = + lbctx.suspense.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); + + test("useUnreadInboxNotificationsCount()", () => { + { + const { count, error, isLoading } = + lbctx.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useUnreadInboxNotificationsCount() (suspense)", () => { + { + const { count, error, isLoading } = + lbctx.suspense.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook (suspense) with branchId parameter -{ - const status = lbctx.suspense.useAiChatStatus("chat-123", "ms_branch" as any); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType + }); + + test("useSyncStatus()", () => { + { + const status = lbctx.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + { + const status = lbctx.suspense.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + { + const status = ctx.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + { + const status = ctx.suspense.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + }); + + test("useNotificationSettings()", () => { + { + const [{ isLoading, error, settings }, update] = + lbctx.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); + + test("useNotificationSettings() (suspense)", () => { + { + const [{ isLoading, error, settings }, update] = + lbctx.suspense.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); + + test("useAiChatStatus()", () => { + { + const status = lbctx.useAiChatStatus("chat-123"); + expectTypeOf(status).toEqualTypeOf(); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() (suspense)", () => { + { + const status = lbctx.suspense.useAiChatStatus("chat-123"); + expectTypeOf(status).toEqualTypeOf(); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() with branchId parameter", () => { + { + const status = lbctx.useAiChatStatus("chat-123", "ms_branch" as any); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() with branchId parameter (suspense)", () => { + { + const status = lbctx.suspense.useAiChatStatus( + "chat-123", + "ms_branch" as any ); - expectType(status.toolName); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// --------------------------------------------------------- + }); +}); diff --git a/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx b/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx index 3ac08f54fbb..da9e28ef1cb 100644 --- a/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx +++ b/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx @@ -1,3 +1,4 @@ +import React from "react"; import type { BaseMetadata, NotificationSettings, @@ -8,1161 +9,1343 @@ import { LiveObject, LiveList } from "@liveblocks/client"; import * as classic from "@liveblocks/react"; import * as suspense from "@liveblocks/react/suspense"; import type { AiChatStatus } from "@liveblocks/react"; -import { expectAssignable, expectError, expectType } from "tsd"; - -// LiveblocksProvider -{ - const LiveblocksProvider = classic.LiveblocksProvider; - - expectError(); - expectError(); - - ; - ; - ; - ({ token: "token" })} - throttle={16} - />; - - [{ foo: "bar" }]} - />; - - [{ name: "Vincent", age: 42 }]} - />; - - ; -} - -// LiveblocksProvider (suspense) -{ - const LiveblocksProvider = suspense.LiveblocksProvider; - - expectError(); - expectError(); - - ; - ; - ; - ({ token: "token" })} - throttle={16} - />; - - [{ foo: "bar" }]} - />; - - [{ name: "Vincent", age: 42 }]} - />; - - ; -} - -// RoomProvider -{ - const RoomProvider = classic.RoomProvider; - - // Missing mandatory room ID is an error - expectError( - -

- - ); - - -
- ; - - -
- ; - - -
- ; - - -
- ; -} - -// RoomProvider (suspense) -{ - const RoomProvider = suspense.RoomProvider; - - // Missing mandatory room ID is an error - expectError( - -
- - ); - - -
- ; - - -
- ; - - -
- ; - - -
- ; -} - -// --------------------------------------------------------- -// Hook APIs -// --------------------------------------------------------- - -// useRoom() -{ - const room = classic.useRoom(); - expectType(room.getPresence().cursor); - expectType(room.getPresence().nonexisting); - - expectType(classic.useRoom({ allowOutsideRoom: false }).id); - expectType( - classic.useRoom({ allowOutsideRoom: true })?.id - ); - expectType( - classic.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id - ); -} - -// useRoom() (suspense) -{ - const room = suspense.useRoom(); - expectType(room.getPresence().cursor); - expectType(room.getPresence().nonexisting); - - expectType(suspense.useRoom({ allowOutsideRoom: false }).id); - expectType( - suspense.useRoom({ allowOutsideRoom: true })?.id - ); - expectType( - suspense.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id - ); -} - -// --------------------------------------------------------- - -// useIsInsideRoom() -{ - const isInsideRoom = classic.useIsInsideRoom(); - expectType(isInsideRoom); -} - -// useIsInsideRoom() (suspense) -{ - const isInsideRoom = suspense.useIsInsideRoom(); - expectType(isInsideRoom); -} - -// --------------------------------------------------------- - -// useErrorListener() -{ - classic.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... - } - }); -} - -// useErrorListener() (suspense) -{ - suspense.useErrorListener((err) => { - expectType(err.message); - expectType(err.stack); - expectType( - err.context.code - ); - expectAssignable< - | "AI_CONNECTION_ERROR" - | "ROOM_CONNECTION_ERROR" - | "CREATE_THREAD_ERROR" - | "DELETE_THREAD_ERROR" - | "EDIT_THREAD_METADATA_ERROR" - | "EDIT_COMMENT_METADATA_ERROR" - | "MARK_THREAD_AS_RESOLVED_ERROR" - | "MARK_THREAD_AS_UNRESOLVED_ERROR" - | "SUBSCRIBE_TO_THREAD_ERROR" - | "UNSUBSCRIBE_FROM_THREAD_ERROR" - | "CREATE_COMMENT_ERROR" - | "EDIT_COMMENT_ERROR" - | "DELETE_COMMENT_ERROR" - | "ADD_REACTION_ERROR" - | "REMOVE_REACTION_ERROR" - | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" - | "DELETE_INBOX_NOTIFICATION_ERROR" - | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" - | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" - | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" - | "UPDATE_NOTIFICATION_SETTINGS_ERROR" - | "LARGE_MESSAGE_ERROR" - | "FEED_REQUEST_ERROR" - >(err.context.type); - if (err.context.type === "ROOM_CONNECTION_ERROR") { - expectAssignable(err.context.code); - expectAssignable(err.context.code); - } else if (err.context.type === "CREATE_THREAD_ERROR") { - expectType(err.context.roomId); - expectType(err.context.threadId); - expectType(err.context.commentId); - } else { - // Not going to list them all... - } - }); -} - -// --------------------------------------------------------- - -// useSelf() -{ - const me = classic.useSelf(); - expectType(me?.presence.cursor); - expectType(me?.presence.nonexisting); - - expectType(me?.info?.name); - expectType(me?.info?.age); - expectType(me?.info?.nonexisting); -} - -// useSelf() (suspense) -{ - const me = suspense.useSelf(); - expectType(me.presence.cursor); - expectType(me.presence.nonexisting); - - expectType(me.info?.name); - expectType(me.info?.age); - expectType(me.info?.nonexisting); -} - -// useSelf(selector) -{ - const x = classic.useSelf((me) => me.presence.cursor); - expectType(x); -} - -// useSelf(selector) (suspense) -{ - const x = suspense.useSelf((me) => me.presence.cursor); - expectType(x); -} - -// --------------------------------------------------------- - -// useOthers() -{ - const others = classic.useOthers(); - expectType(others[13]!.presence.cursor); - expectType(others[0]!.canWrite); -} - -// useOthers() (suspense) -{ - const others = suspense.useOthers(); - expectType(others[13]!.presence.cursor); - expectType(others[0]!.canWrite); -} - -// useOthers(selector) -{ - const num = classic.useOthers((others) => others.length); - expectType(num); - - const xs = classic.useOthers((others) => - others.map((o) => o.presence.cursor) - ); - expectType<(Json | undefined)[]>(xs); -} - -// useOthers(selector) (suspense) -{ - const num = classic.useOthers((others) => others.length); - expectType(num); - - const xs = classic.useOthers((others) => - others.map((o) => o.presence.cursor) - ); - expectType<(Json | undefined)[]>(xs); -} - -// useOthers(selector, eq) -{ - const xs = classic.useOthers( - (others) => others.map((o) => o.presence.cursor), - classic.shallow - ); - expectType<(Json | undefined)[]>(xs); -} - -// useOthers(selector, eq) (suspense) -{ - const xs = suspense.useOthers( - (others) => others.map((o) => o.presence.cursor), - suspense.shallow - ); - expectType<(Json | undefined)[]>(xs); -} - -// --------------------------------------------------------- - -// The useMutation() hook -{ - expectType<(a: number, b: boolean) => "hi">( - classic.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor); - expectType(mut.self.presence.nonexisting); - expectType(mut.self.info!.name); - expectType(mut.self.info!.age); - expectType(mut.self.info!.nonexisting); - - expectType(mut.others[0]!.presence.cursor); - expectType(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info!.name); - expectType(mut.others[0]!.info!.age); - expectType(mut.others[0]!.info!.nonexisting); - - expectType(mut.storage.get("animals")); - expectType(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectType(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} - -// The useMutation() hook (suspense) -{ - expectType<(a: number, b: boolean) => "hi">( - suspense.useMutation((mut, _a: number, _b: boolean) => { - expectType(mut.self.presence.cursor); - expectType(mut.self.presence.nonexisting); - expectType(mut.self.info!.name); - expectType(mut.self.info!.age); - expectType(mut.self.info!.nonexisting); - - expectType(mut.others[0]!.presence.cursor); - expectType(mut.others[0]!.presence.nonexisting); - expectType(mut.others[0]!.info!.name); - expectType(mut.others[0]!.info!.age); - expectType(mut.others[0]!.info!.nonexisting); - - expectType(mut.storage.get("animals")); - expectType(mut.storage.get("nonexisting")); - expectType(mut.setMyPresence({ cursor: { x: 0, y: 0 } })); - expectType(mut.setMyPresence({ nonexisting: 123 })); - - return "hi" as const; - }, []) - ); -} - -// --------------------------------------------------------- - -// useBroadcastEvent() -{ - const broadcast = classic.useBroadcastEvent(); - broadcast({ type: "emoji", emoji: "😍" }); - broadcast({ type: "left", userId: "1234" }); - broadcast({ a: [], b: "", c: 123, d: false, e: undefined, f: null }); // arbitrary JSON - expectError(broadcast({ notSerializable: new Date() })); - expectError(broadcast(new Date())); -} - -// useBroadcastEvent() (suspense) -{ - const broadcast = suspense.useBroadcastEvent(); - broadcast({ type: "emoji", emoji: "😍" }); - broadcast({ type: "left", userId: "1234" }); - broadcast({ a: [], b: "", c: 123, d: false, e: undefined, f: null }); // arbitrary JSON - expectError(broadcast({ notSerializable: new Date() })); - expectError(broadcast(new Date())); -} - -// --------------------------------------------------------- - -// The useUser() hook -{ - const { user, error, isLoading } = classic.useUser("user-id"); - expectType(isLoading); - expectType(user?.name); - expectType(user?.avatar); - expectType(user?.age); - expectType(error); -} - -// The useUser() hook (suspense) -{ - const { user, error, isLoading } = suspense.useUser("user-id"); - expectType(isLoading); - expectType(user?.name); - expectType(user?.avatar); - expectType(user?.age); - expectType(error); -} - -// --------------------------------------------------------- - -// The useRoomInfo() hook -{ - const { info, error, isLoading } = classic.useRoomInfo("room-id"); - expectType(isLoading); - expectType(info?.name); - expectType(info?.url); - expectType(info?.nonexisting); - expectType(error); -} - -// The useRoomInfo() hook (suspense) -{ - const { info, error, isLoading } = suspense.useRoomInfo("room-id"); - expectType(isLoading); - expectType(info.name); - expectType(info.url); - expectType(info?.nonexisting); - expectType(error); -} - -// --------------------------------------------------------- - -// The useGroupInfo() hook -{ - const { info, error, isLoading } = classic.useGroupInfo("group-id"); - expectType(isLoading); - expectType(info?.name); - expectType(info?.avatar); - expectType(info?.nonexisting); - expectType(error); -} - -// The useGroupInfo() hook (suspense) -{ - const { info, error, isLoading } = suspense.useGroupInfo("group-id"); - expectType(isLoading); - expectType(info.name); - expectType(info.avatar); - expectType(info?.nonexisting); - expectType(error); -} - -// --------------------------------------------------------- - -// The useCreateThread() hook -{ - const createThread = classic.useCreateThread(); - expectError(createThread({})); - - const thread1 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); - - expectType<"thread">(thread1.type); - expectType(thread1.id); - expectType(thread1.roomId); - expectType<"comment">(thread1.comments[0]!.type); - expectType(thread1.comments[0]!.id); - expectType(thread1.comments[0]!.threadId); - - expectType(thread1.metadata.color); - - const thread2 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { foo: "bar" }, - }); - - expectType(thread2.id); - expectType(thread2.metadata.foo); - expectType( - thread2.metadata.nonexisting - ); -} - -// The useCreateThread() hook (suspense) -{ - const createThread = suspense.useCreateThread(); - expectError(createThread({})); - - const thread1 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); - - expectType<"thread">(thread1.type); - expectType(thread1.id); - expectType(thread1.roomId); - expectType<"comment">(thread1.comments[0]!.type); - expectType(thread1.comments[0]!.id); - expectType(thread1.comments[0]!.threadId); - - expectType(thread1.metadata.foo); - - const thread2 = createThread({ - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { foo: "bar" }, - }); - - expectType(thread2.id); - expectType(thread2.metadata.foo); - expectType( - thread2.metadata.nonexisting - ); -} - -// --------------------------------------------------------- - -// The useEditThreadMetadata() hook -{ - const editMetadata = classic.useEditThreadMetadata(); - expectError(editMetadata({})); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); -} - -// The useEditThreadMetadata() hook (suspense) -{ - // --------------------- - const editMetadata = suspense.useEditThreadMetadata(); - expectError(editMetadata({})); // no body = error - - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) - ); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) - ); - - expectType(editMetadata({ threadId: "th_xxx", metadata: {} })); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { color: null } }) - ); - expectType( - editMetadata({ threadId: "th_xxx", metadata: { color: "red" } }) - ); -} - -// --------------------------------------------------------- - -// The useCreateComment() hook -{ - { - const createComment = classic.useCreateComment(); - expectError(createComment({})); - - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); - - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - expectType(comment.metadata.foo); - - const commentWithMetadata = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { status: "pending", priority: 1 }, - }); - - expectType<"comment">(commentWithMetadata.type); - expectType( - commentWithMetadata.metadata.status - ); - expectType( - commentWithMetadata.metadata.priority - ); - } -} - -// The useCreateComment() hook (suspense) -{ - const createComment = suspense.useCreateComment(); - expectError(createComment({})); - - const comment = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - }); - - expectType<"comment">(comment.type); - expectType(comment.id); - expectType(comment.threadId); - expectType(comment.metadata.foo); - - const commentWithMetadata = createComment({ - threadId: "th_xxx", - body: { - version: 1, - content: [{ type: "paragraph", children: [{ text: "hi" }] }], - }, - metadata: { priority: 2 }, - }); - - expectType<"comment">(commentWithMetadata.type); - expectType( - commentWithMetadata.metadata.status - ); -} - -// --------------------------------------------------------- - -// The useEditComment() hook -{ - const editComment = classic.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2 }, - }) - ); -} - -// The useEditComment() hook (suspense) -{ - const editComment = suspense.useEditComment(); - expectError(editComment({})); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - }) - ); - - expectType( - editComment({ - threadId: "th_xxx", - commentId: "cm_xxx", - body: { version: 1, content: [] }, - metadata: { priority: 2 }, - }) - ); -} - -// --------------------------------------------------------- - -// The useEditCommentMetadata() hook -{ - const editMetadata = classic.useEditCommentMetadata(); - expectError(editMetadata({})); - - expectType( - editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: 123 }, - }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: null }, - }) - ); -} - -// The useEditCommentMetadata() hook (suspense) -{ - const editMetadata = suspense.useEditCommentMetadata(); - expectError(editMetadata({})); - - expectType( - editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: null }, - }) - ); - expectType( - editMetadata({ - threadId: "th_xxx", - commentId: "cm_xxx", - metadata: { nonexisting: 123 }, - }) - ); -} - -// --------------------------------------------------------- - -// The useDeleteComment() hook -{ - const deleteComment = classic.useDeleteComment(); - - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); - - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} - -// The useDeleteComment() hook (suspense) -{ - const deleteComment = suspense.useDeleteComment(); - - expectError(deleteComment({})); - expectError(deleteComment({ threadId: "th_xxx" })); - expectError(deleteComment({ commentId: "co_xxx" })); - - expectType(deleteComment({ threadId: "th_xxx", commentId: "co_xxx" })); -} - -// --------------------------------------------------------- - -// The useAddReaction() hook -{ - const addReaction = classic.useAddReaction(); - - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// The useAddReaction() hook (suspense) -{ - const addReaction = suspense.useAddReaction(); - - expectError(addReaction({})); - expectError(addReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(addReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - addReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// --------------------------------------------------------- - -// The useRemoveReaction() hook -{ - const removeReaction = classic.useRemoveReaction(); - - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// The useRemoveReaction() hook (suspense) -{ - const removeReaction = suspense.useRemoveReaction(); - - expectError(removeReaction({})); - expectError(removeReaction({ threadId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ commentId: "th_xxx", emoji: "👍" })); - expectError(removeReaction({ threadId: "th_xxx", commentId: "th_xxx" })); - - expectType( - removeReaction({ - threadId: "th_xxx", - commentId: "cm_xxx", - emoji: "👍", - }) - ); -} - -// --------------------------------------------------------- - -// The useInboxNotifications() hook -{ - expectType(classic.useInboxNotifications().isLoading); - expectType(classic.useInboxNotifications().error); - expectType<("thread" | "textMention" | `$${string}`)[] | undefined>( - classic.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[] | undefined>( - classic.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.roomId) - ); -} - -// The useInboxNotifications() hook (suspense) -{ - expectType(suspense.useInboxNotifications().isLoading); - expectType(suspense.useInboxNotifications().error); - expectType<("thread" | "textMention" | `$${string}`)[]>( - suspense.useInboxNotifications().inboxNotifications?.map((ibn) => ibn.kind) - ); - expectType<(string | undefined)[]>( - suspense - .useInboxNotifications() - .inboxNotifications?.map((ibn) => ibn.roomId) - ); -} - -// --------------------------------------------------------- - -// The useInboxNotificationThread() hook -{ - const result = classic.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType(result.metadata); - expectType(result.metadata.color); - expectType( - result.metadata.nonexisting - ); -} - -// The useInboxNotificationThread() hook (suspense) -{ - const result = suspense.useInboxNotificationThread("in_xxx"); - expectType<"thread">(result.type); - expectType(result.roomId); - expectAssignable(result.comments); - expectType(result.metadata); - expectType(result.metadata.color); - expectType( - result.metadata.nonexisting - ); -} - -// --------------------------------------------------------- - -// The useMarkInboxNotificationAsRead() hook -{ - const markRead = classic.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} - -// The useMarkInboxNotificationAsRead() hook (suspense) -{ - const markRead = suspense.useMarkInboxNotificationAsRead(); - expectType(markRead("in_xxx")); -} - -// --------------------------------------------------------- - -// The useMarkAllInboxNotificationsAsRead() hook -{ - const markAllRead = classic.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} - -// The useMarkAllInboxNotificationsAsRead() hook (suspense) -{ - const markAllRead = suspense.useMarkAllInboxNotificationsAsRead(); - expectType(markAllRead()); -} - -// --------------------------------------------------------- - -// The useDeleteInboxNotification() hook -{ - const deleteNotification = classic.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} - -// The useDeleteInboxNotification() hook (suspense) -{ - const deleteNotification = suspense.useDeleteInboxNotification(); - expectType(deleteNotification("in_xxx")); -} - -// --------------------------------------------------------- - -// The useDeleteAllInboxNotifications() hook -{ - const deleteAllNotifications = classic.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} - -// The useDeleteAllInboxNotifications() hook (suspense) -{ - const deleteAllNotifications = suspense.useDeleteAllInboxNotifications(); - expectType(deleteAllNotifications()); -} - -// --------------------------------------------------------- - -// The useUnreadInboxNotificationsCount() hook -{ - const { count, error, isLoading } = - classic.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} - -// The useUnreadInboxNotificationsCount() hook (suspense) -{ - const { count, error, isLoading } = - suspense.useUnreadInboxNotificationsCount(); - expectType(isLoading); - expectType(count); - expectType(error); -} - -// --------------------------------------------------------- - -// The useSyncStatus() hook -{ - const status = classic.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} -{ - const status = suspense.useSyncStatus(); - expectType<"synchronizing" | "synchronized">(status); -} - -// --------------------------------------------------------- -// the useNotificationSettings() hook -{ - const [{ isLoading, error, settings }, update] = - classic.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// the useNotificationSettings() hook suspense -{ - const [{ isLoading, error, settings }, update] = - suspense.useNotificationSettings(); - expectType(isLoading); - expectType(error); - expectType(settings); - expectType(update({})); // empty {} because of partial definition -} -// --------------------------------------------------------- - -// The useAiChatStatus() hook -{ - const status = classic.useAiChatStatus("chat-123"); - expectType(status); - - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType +import { describe, expectTypeOf, test } from "vitest"; + +describe("without Liveblocks augmentation", () => { + test("LiveblocksProvider", () => { + { + const LiveblocksProvider = classic.LiveblocksProvider; + + void ( + ( + // @ts-expect-error + + ) ); - expectType(status.toolName); - } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook (suspense) -{ - const status = suspense.useAiChatStatus("chat-123"); - expectType(status); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType + void ( + ( + // @ts-expect-error + + ) ); - expectType(status.toolName); - } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook with branchId parameter -{ - const status = classic.useAiChatStatus("chat-123", "ms_branch" as any); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType + + ; + ; + ; + ({ token: "token" })} + throttle={16} + />; + + [{ foo: "bar" }]} + />; + + [{ name: "Vincent", age: 42 }]} + />; + + ; + } + }); + + test("LiveblocksProvider (suspense)", () => { + { + const LiveblocksProvider = suspense.LiveblocksProvider; + + void ( + ( + // @ts-expect-error + + ) + ); + void ( + ( + // @ts-expect-error + + ) + ); + + ; + ; + ; + ({ token: "token" })} + throttle={16} + />; + + [{ foo: "bar" }]} + />; + + [{ name: "Vincent", age: 42 }]} + />; + + ; + } + }); + + test("RoomProvider", () => { + { + const RoomProvider = classic.RoomProvider; + + // Missing mandatory room ID is an error + void ( + ( + // @ts-expect-error + +
+ + ) ); - expectType(status.toolName); - } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} - -// The useAiChatStatus() hook with branchId parameter -{ - const status = suspense.useAiChatStatus("chat-123", "ms_branch" as any); - if (status.status === "generating") { - expectType<"generating">(status.status); - if (status.partType === "tool-invocation") { - expectType<"tool-invocation">(status.partType); - expectType(status.toolName); - } else { - expectType<"text" | "reasoning" | "retrieval" | "sources" | undefined>( - status.partType + + +
+ ; + + +
+ ; + + +
+ ; + + +
+ ; + } + }); + + test("RoomProvider (suspense)", () => { + { + const RoomProvider = suspense.RoomProvider; + + // Missing mandatory room ID is an error + void ( + ( + // @ts-expect-error + +
+ + ) ); - expectType(status.toolName); + + +
+ ; + + +
+ ; + + +
+ ; + + +
+ ; + } + }); + + test("useRoom()", () => { + { + const room = classic.useRoom(); + expectTypeOf(room.getPresence().cursor).toEqualTypeOf(); + expectTypeOf(room.getPresence().nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf( + classic.useRoom({ allowOutsideRoom: false }).id + ).toEqualTypeOf(); + expectTypeOf( + classic.useRoom({ allowOutsideRoom: true })?.id + ).toEqualTypeOf(); + expectTypeOf( + classic.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id + ).toEqualTypeOf(); + } + }); + + test("useRoom() (suspense)", () => { + { + const room = suspense.useRoom(); + expectTypeOf(room.getPresence().cursor).toEqualTypeOf(); + expectTypeOf(room.getPresence().nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: false }).id + ).toEqualTypeOf(); + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: true })?.id + ).toEqualTypeOf(); + expectTypeOf( + suspense.useRoom({ allowOutsideRoom: Math.random() < 0.5 })?.id + ).toEqualTypeOf(); + } + }); + + test("useIsInsideRoom()", () => { + { + const isInsideRoom = classic.useIsInsideRoom(); + expectTypeOf(isInsideRoom).toEqualTypeOf(); + } + }); + + test("useIsInsideRoom() (suspense)", () => { + { + const isInsideRoom = suspense.useIsInsideRoom(); + expectTypeOf(isInsideRoom).toEqualTypeOf(); + } + }); + + test("useErrorListener()", () => { + { + classic.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + } + }); + + test("useErrorListener() (suspense)", () => { + { + suspense.useErrorListener((err) => { + expectTypeOf(err.message).toEqualTypeOf(); + expectTypeOf(err.stack).toEqualTypeOf(); + expectTypeOf(err.context.code).toEqualTypeOf< + string | -1 | 4001 | 4005 | 4006 | (number & {}) | undefined + >(); + expectTypeOf(err.context.type).toExtend< + | "AI_CONNECTION_ERROR" + | "ROOM_CONNECTION_ERROR" + | "CREATE_THREAD_ERROR" + | "DELETE_THREAD_ERROR" + | "EDIT_THREAD_METADATA_ERROR" + | "EDIT_COMMENT_METADATA_ERROR" + | "MARK_THREAD_AS_RESOLVED_ERROR" + | "MARK_THREAD_AS_UNRESOLVED_ERROR" + | "SUBSCRIBE_TO_THREAD_ERROR" + | "UNSUBSCRIBE_FROM_THREAD_ERROR" + | "CREATE_COMMENT_ERROR" + | "EDIT_COMMENT_ERROR" + | "DELETE_COMMENT_ERROR" + | "ADD_REACTION_ERROR" + | "REMOVE_REACTION_ERROR" + | "MARK_INBOX_NOTIFICATION_AS_READ_ERROR" + | "DELETE_INBOX_NOTIFICATION_ERROR" + | "MARK_ALL_INBOX_NOTIFICATIONS_AS_READ_ERROR" + | "DELETE_ALL_INBOX_NOTIFICATIONS_ERROR" + | "UPDATE_ROOM_SUBSCRIPTION_SETTINGS_ERROR" + | "UPDATE_NOTIFICATION_SETTINGS_ERROR" + | "LARGE_MESSAGE_ERROR" + | "FEED_REQUEST_ERROR" + >(); + if (err.context.type === "ROOM_CONNECTION_ERROR") { + expectTypeOf(err.context.code).toExtend(); + expectTypeOf(err.context.code).toExtend(); + } else if (err.context.type === "CREATE_THREAD_ERROR") { + expectTypeOf(err.context.roomId).toEqualTypeOf(); + expectTypeOf(err.context.threadId).toEqualTypeOf(); + expectTypeOf(err.context.commentId).toEqualTypeOf(); + } else { + // Not going to list them all... + } + }); + } + }); + + test("useSelf()", () => { + { + const me = classic.useSelf(); + expectTypeOf(me?.presence.cursor).toEqualTypeOf(); + expectTypeOf(me?.presence.nonexisting).toEqualTypeOf(); + + expectTypeOf(me?.info?.name).toEqualTypeOf(); + expectTypeOf(me?.info?.age).toEqualTypeOf(); + expectTypeOf(me?.info?.nonexisting).toEqualTypeOf(); + } + }); + + test("useSelf() (suspense)", () => { + { + const me = suspense.useSelf(); + expectTypeOf(me.presence.cursor).toEqualTypeOf(); + expectTypeOf(me.presence.nonexisting).toEqualTypeOf(); + + expectTypeOf(me.info?.name).toEqualTypeOf(); + expectTypeOf(me.info?.age).toEqualTypeOf(); + expectTypeOf(me.info?.nonexisting).toEqualTypeOf(); + } + }); + + test("useSelf(selector)", () => { + { + const x = classic.useSelf((me) => me.presence.cursor); + expectTypeOf(x).toEqualTypeOf(); + } + }); + + test("useSelf(selector) (suspense)", () => { + { + const x = suspense.useSelf((me) => me.presence.cursor); + expectTypeOf(x).toEqualTypeOf(); + } + }); + + test("useOthers()", () => { + { + const others = classic.useOthers(); + expectTypeOf(others[13]!.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(others[0]!.canWrite).toEqualTypeOf(); + } + }); + + test("useOthers() (suspense)", () => { + { + const others = suspense.useOthers(); + expectTypeOf(others[13]!.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(others[0]!.canWrite).toEqualTypeOf(); + } + }); + + test("useOthers(selector)", () => { + { + const num = classic.useOthers((others) => others.length); + expectTypeOf(num).toEqualTypeOf(); + + const xs = classic.useOthers((others) => + others.map((o) => o.presence.cursor) + ); + expectTypeOf(xs).toEqualTypeOf<(Json | undefined)[]>(); + } + }); + + test("useOthers(selector) (suspense)", () => { + { + const num = classic.useOthers((others) => others.length); + expectTypeOf(num).toEqualTypeOf(); + + const xs = classic.useOthers((others) => + others.map((o) => o.presence.cursor) + ); + expectTypeOf(xs).toEqualTypeOf<(Json | undefined)[]>(); + } + }); + + test("useOthers(selector, eq)", () => { + { + const xs = classic.useOthers( + (others) => others.map((o) => o.presence.cursor), + classic.shallow + ); + expectTypeOf(xs).toEqualTypeOf<(Json | undefined)[]>(); + } + }); + + test("useOthers(selector, eq) (suspense)", () => { + { + const xs = suspense.useOthers( + (others) => others.map((o) => o.presence.cursor), + suspense.shallow + ); + expectTypeOf(xs).toEqualTypeOf<(Json | undefined)[]>(); + } + }); + + test("useMutation()", () => { + { + expectTypeOf( + classic.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.self.presence.nonexisting).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.self.info!.name).toEqualTypeOf(); + expectTypeOf(mut.self.info!.age).toEqualTypeOf(); + expectTypeOf(mut.self.info!.nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf(mut.others[0]!.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.presence.nonexisting).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.info!.name).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.others[0]!.info!.age).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.info!.nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf(mut.storage.get("animals")).toEqualTypeOf< + Lson | undefined + >(); + expectTypeOf(mut.storage.get("nonexisting")).toEqualTypeOf< + Lson | undefined + >(); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + expectTypeOf( + mut.setMyPresence({ nonexisting: 123 }) + ).toEqualTypeOf(); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } + }); + + test("useMutation() (suspense)", () => { + { + expectTypeOf( + suspense.useMutation((mut, _a: number, _b: boolean) => { + expectTypeOf(mut.self.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.self.presence.nonexisting).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.self.info!.name).toEqualTypeOf(); + expectTypeOf(mut.self.info!.age).toEqualTypeOf(); + expectTypeOf(mut.self.info!.nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf(mut.others[0]!.presence.cursor).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.presence.nonexisting).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.info!.name).toEqualTypeOf< + string | undefined + >(); + expectTypeOf(mut.others[0]!.info!.age).toEqualTypeOf< + Json | undefined + >(); + expectTypeOf(mut.others[0]!.info!.nonexisting).toEqualTypeOf< + Json | undefined + >(); + + expectTypeOf(mut.storage.get("animals")).toEqualTypeOf< + Lson | undefined + >(); + expectTypeOf(mut.storage.get("nonexisting")).toEqualTypeOf< + Lson | undefined + >(); + expectTypeOf( + mut.setMyPresence({ cursor: { x: 0, y: 0 } }) + ).toEqualTypeOf(); + expectTypeOf( + mut.setMyPresence({ nonexisting: 123 }) + ).toEqualTypeOf(); + + return "hi" as const; + }, []) + ).toEqualTypeOf<(a: number, b: boolean) => "hi">(); + } + }); + + test("useBroadcastEvent()", () => { + { + const broadcast = classic.useBroadcastEvent(); + broadcast({ type: "emoji", emoji: "😍" }); + broadcast({ type: "left", userId: "1234" }); + broadcast({ a: [], b: "", c: 123, d: false, e: undefined, f: null }); // arbitrary JSON + // @ts-expect-error + void broadcast({ notSerializable: new Date() }); + // @ts-expect-error + void broadcast(new Date()); + } + }); + + test("useBroadcastEvent() (suspense)", () => { + { + const broadcast = suspense.useBroadcastEvent(); + broadcast({ type: "emoji", emoji: "😍" }); + broadcast({ type: "left", userId: "1234" }); + broadcast({ a: [], b: "", c: 123, d: false, e: undefined, f: null }); // arbitrary JSON + // @ts-expect-error + void broadcast({ notSerializable: new Date() }); + // @ts-expect-error + void broadcast(new Date()); + } + }); + + test("useUser()", () => { + { + const { user, error, isLoading } = classic.useUser("user-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user?.name).toEqualTypeOf(); + expectTypeOf(user?.avatar).toEqualTypeOf(); + expectTypeOf(user?.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useUser() (suspense)", () => { + { + const { user, error, isLoading } = suspense.useUser("user-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(user?.name).toEqualTypeOf(); + expectTypeOf(user?.avatar).toEqualTypeOf(); + expectTypeOf(user?.age).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useRoomInfo()", () => { + { + const { info, error, isLoading } = classic.useRoomInfo("room-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.url).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useRoomInfo() (suspense)", () => { + { + const { info, error, isLoading } = suspense.useRoomInfo("room-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.url).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useGroupInfo()", () => { + { + const { info, error, isLoading } = classic.useGroupInfo("group-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info?.name).toEqualTypeOf(); + expectTypeOf(info?.avatar).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useGroupInfo() (suspense)", () => { + { + const { info, error, isLoading } = suspense.useGroupInfo("group-id"); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(info.name).toEqualTypeOf(); + expectTypeOf(info.avatar).toEqualTypeOf(); + expectTypeOf(info?.nonexisting).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useCreateThread()", () => { + { + const createThread = classic.useCreateThread(); + // @ts-expect-error + void createThread({}); + + const thread1 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(thread1.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread1.id).toEqualTypeOf(); + expectTypeOf(thread1.roomId).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread1.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread1.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + + const thread2 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { foo: "bar" }, + }); + + expectTypeOf(thread2.id).toEqualTypeOf(); + expectTypeOf(thread2.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread2.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + }); + + test("useCreateThread() (suspense)", () => { + { + const createThread = suspense.useCreateThread(); + // @ts-expect-error + void createThread({}); + + const thread1 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(thread1.type).toEqualTypeOf<"thread">(); + expectTypeOf(thread1.id).toEqualTypeOf(); + expectTypeOf(thread1.roomId).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.type).toEqualTypeOf<"comment">(); + expectTypeOf(thread1.comments[0]!.id).toEqualTypeOf(); + expectTypeOf(thread1.comments[0]!.threadId).toEqualTypeOf(); + + expectTypeOf(thread1.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + + const thread2 = createThread({ + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { foo: "bar" }, + }); + + expectTypeOf(thread2.id).toEqualTypeOf(); + expectTypeOf(thread2.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(thread2.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + }); + + test("useEditThreadMetadata()", () => { + { + const editMetadata = classic.useEditThreadMetadata(); + // @ts-expect-error + void editMetadata({}); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) + ).toEqualTypeOf(); + } + }); + + test("useEditThreadMetadata() (suspense)", () => { + { + // --------------------- + const editMetadata = suspense.useEditThreadMetadata(); + // @ts-expect-error + void editMetadata({}); // no body = error + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: null } }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { nonexisting: 123 } }) + ).toEqualTypeOf(); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { color: null } }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ threadId: "th_xxx", metadata: { color: "red" } }) + ).toEqualTypeOf(); + } + }); + + test("useCreateComment()", () => { + { + { + const createComment = classic.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + expectTypeOf(comment.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + + const commentWithMetadata = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { status: "pending", priority: 1 }, + }); + + expectTypeOf(commentWithMetadata.type).toEqualTypeOf<"comment">(); + expectTypeOf(commentWithMetadata.metadata.status).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(commentWithMetadata.metadata.priority).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + } + }); + + test("useCreateComment() (suspense)", () => { + { + const createComment = suspense.useCreateComment(); + // @ts-expect-error + void createComment({}); + + const comment = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + }); + + expectTypeOf(comment.type).toEqualTypeOf<"comment">(); + expectTypeOf(comment.id).toEqualTypeOf(); + expectTypeOf(comment.threadId).toEqualTypeOf(); + expectTypeOf(comment.metadata.foo).toEqualTypeOf< + string | number | boolean | undefined + >(); + + const commentWithMetadata = createComment({ + threadId: "th_xxx", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "hi" }] }], + }, + metadata: { priority: 2 }, + }); + + expectTypeOf(commentWithMetadata.type).toEqualTypeOf<"comment">(); + expectTypeOf(commentWithMetadata.metadata.status).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + }); + + test("useEditComment()", () => { + { + const editComment = classic.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2 }, + }) + ).toEqualTypeOf(); + } + }); + + test("useEditComment() (suspense)", () => { + { + const editComment = suspense.useEditComment(); + // @ts-expect-error + void editComment({}); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + }) + ).toEqualTypeOf(); + + expectTypeOf( + editComment({ + threadId: "th_xxx", + commentId: "cm_xxx", + body: { version: 1, content: [] }, + metadata: { priority: 2 }, + }) + ).toEqualTypeOf(); + } + }); + + test("useEditCommentMetadata()", () => { + { + const editMetadata = classic.useEditCommentMetadata(); + // @ts-expect-error + void editMetadata({}); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { nonexisting: 123 }, + }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { nonexisting: null }, + }) + ).toEqualTypeOf(); + } + }); + + test("useEditCommentMetadata() (suspense)", () => { + { + const editMetadata = suspense.useEditCommentMetadata(); + // @ts-expect-error + void editMetadata({}); + + expectTypeOf( + editMetadata({ threadId: "th_xxx", commentId: "cm_xxx", metadata: {} }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { nonexisting: null }, + }) + ).toEqualTypeOf(); + expectTypeOf( + editMetadata({ + threadId: "th_xxx", + commentId: "cm_xxx", + metadata: { nonexisting: 123 }, + }) + ).toEqualTypeOf(); + } + }); + + test("useDeleteComment()", () => { + { + const deleteComment = classic.useDeleteComment(); + + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); + + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); } - } else { - expectType<"disconnected" | "loading" | "idle">(status.status); - expectType(status.partType); - expectType(status.toolName); - } -} + }); + + test("useDeleteComment() (suspense)", () => { + { + const deleteComment = suspense.useDeleteComment(); + + // @ts-expect-error + void deleteComment({}); + // @ts-expect-error + void deleteComment({ threadId: "th_xxx" }); + // @ts-expect-error + void deleteComment({ commentId: "co_xxx" }); + + expectTypeOf( + deleteComment({ threadId: "th_xxx", commentId: "co_xxx" }) + ).toEqualTypeOf(); + } + }); + + test("useAddReaction()", () => { + { + const addReaction = classic.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useAddReaction() (suspense)", () => { + { + const addReaction = suspense.useAddReaction(); + + // @ts-expect-error + void addReaction({}); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void addReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + addReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useRemoveReaction()", () => { + { + const removeReaction = classic.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useRemoveReaction() (suspense)", () => { + { + const removeReaction = suspense.useRemoveReaction(); + + // @ts-expect-error + void removeReaction({}); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ commentId: "th_xxx", emoji: "👍" }); + // @ts-expect-error + void removeReaction({ threadId: "th_xxx", commentId: "th_xxx" }); + + expectTypeOf( + removeReaction({ + threadId: "th_xxx", + commentId: "cm_xxx", + emoji: "👍", + }) + ).toEqualTypeOf(); + } + }); + + test("useInboxNotifications()", () => { + { + expectTypeOf( + classic.useInboxNotifications().isLoading + ).toEqualTypeOf(); + expectTypeOf(classic.useInboxNotifications().error).toEqualTypeOf< + Error | undefined + >(); + expectTypeOf( + classic + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf< + ("thread" | "textMention" | `$${string}`)[] | undefined + >(); + expectTypeOf( + classic + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[] | undefined>(); + } + }); + + test("useInboxNotifications() (suspense)", () => { + { + expectTypeOf( + suspense.useInboxNotifications().isLoading + ).toEqualTypeOf(); + expectTypeOf( + suspense.useInboxNotifications().error + ).toEqualTypeOf(); + expectTypeOf( + suspense + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.kind) + ).toEqualTypeOf<("thread" | "textMention" | `$${string}`)[]>(); + expectTypeOf( + suspense + .useInboxNotifications() + .inboxNotifications?.map((ibn) => ibn.roomId) + ).toEqualTypeOf<(string | undefined)[]>(); + } + }); + + test("useInboxNotificationThread()", () => { + { + const result = classic.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata).toEqualTypeOf(); + expectTypeOf(result.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(result.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + }); -// --------------------------------------------------------- + test("useInboxNotificationThread() (suspense)", () => { + { + const result = suspense.useInboxNotificationThread("in_xxx"); + expectTypeOf(result.type).toEqualTypeOf<"thread">(); + expectTypeOf(result.roomId).toEqualTypeOf(); + expectTypeOf(result.comments).toExtend(); + expectTypeOf(result.metadata).toEqualTypeOf(); + expectTypeOf(result.metadata.color).toEqualTypeOf< + string | number | boolean | undefined + >(); + expectTypeOf(result.metadata.nonexisting).toEqualTypeOf< + string | number | boolean | undefined + >(); + } + }); + + test("useMarkInboxNotificationAsRead()", () => { + { + const markRead = classic.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); + + test("useMarkInboxNotificationAsRead() (suspense)", () => { + { + const markRead = suspense.useMarkInboxNotificationAsRead(); + expectTypeOf(markRead("in_xxx")).toEqualTypeOf(); + } + }); + + test("useMarkAllInboxNotificationsAsRead()", () => { + { + const markAllRead = classic.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); + + test("useMarkAllInboxNotificationsAsRead() (suspense)", () => { + { + const markAllRead = suspense.useMarkAllInboxNotificationsAsRead(); + expectTypeOf(markAllRead()).toEqualTypeOf(); + } + }); + + test("useDeleteInboxNotification()", () => { + { + const deleteNotification = classic.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); + + test("useDeleteInboxNotification() (suspense)", () => { + { + const deleteNotification = suspense.useDeleteInboxNotification(); + expectTypeOf(deleteNotification("in_xxx")).toEqualTypeOf(); + } + }); + + test("useDeleteAllInboxNotifications()", () => { + { + const deleteAllNotifications = classic.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); + + test("useDeleteAllInboxNotifications() (suspense)", () => { + { + const deleteAllNotifications = suspense.useDeleteAllInboxNotifications(); + expectTypeOf(deleteAllNotifications()).toEqualTypeOf(); + } + }); + + test("useUnreadInboxNotificationsCount()", () => { + { + const { count, error, isLoading } = + classic.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useUnreadInboxNotificationsCount() (suspense)", () => { + { + const { count, error, isLoading } = + suspense.useUnreadInboxNotificationsCount(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(count).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + } + }); + + test("useSyncStatus()", () => { + { + const status = classic.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + { + const status = suspense.useSyncStatus(); + expectTypeOf(status).toEqualTypeOf<"synchronizing" | "synchronized">(); + } + }); + + test("useNotificationSettings()", () => { + { + const [{ isLoading, error, settings }, update] = + classic.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); + + test("useNotificationSettings() (suspense)", () => { + { + const [{ isLoading, error, settings }, update] = + suspense.useNotificationSettings(); + expectTypeOf(isLoading).toEqualTypeOf(); + expectTypeOf(error).toEqualTypeOf(); + expectTypeOf(settings).toEqualTypeOf(); + expectTypeOf(update({})).toEqualTypeOf(); // empty {} because of partial definition + } + }); + + test("useAiChatStatus()", () => { + { + const status = classic.useAiChatStatus("chat-123"); + expectTypeOf(status).toEqualTypeOf(); + + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() (suspense)", () => { + { + const status = suspense.useAiChatStatus("chat-123"); + expectTypeOf(status).toEqualTypeOf(); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() with branchId parameter", () => { + { + const status = classic.useAiChatStatus("chat-123", "ms_branch" as any); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); + + test("useAiChatStatus() with branchId parameter (suspense)", () => { + { + const status = suspense.useAiChatStatus("chat-123", "ms_branch" as any); + if (status.status === "generating") { + expectTypeOf(status.status).toEqualTypeOf<"generating">(); + if (status.partType === "tool-invocation") { + expectTypeOf(status.partType).toEqualTypeOf<"tool-invocation">(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } else { + expectTypeOf(status.partType).toEqualTypeOf< + "text" | "reasoning" | "retrieval" | "sources" | undefined + >(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } else { + expectTypeOf(status.status).toEqualTypeOf< + "disconnected" | "loading" | "idle" + >(); + expectTypeOf(status.partType).toEqualTypeOf(); + expectTypeOf(status.toolName).toEqualTypeOf(); + } + } + }); +}); diff --git a/packages/liveblocks-react/vitest.config.typecheck.ts b/packages/liveblocks-react/vitest.config.typecheck.ts new file mode 100644 index 00000000000..c42d8c40647 --- /dev/null +++ b/packages/liveblocks-react/vitest.config.typecheck.ts @@ -0,0 +1,32 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +// Multiple configs are used when testing augmentation and global +// declerations, to avoid leaking globals between tests. +export default defineConfig({ + test: { + projects: [ + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/factories.test-d.tsx"], + "factories" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/augmentation.test-d.tsx"], + "augmentation" + ), + }, + { + test: makeTypecheckTestConfig( + import.meta, + ["test-d/no-augmentation.test-d.tsx"], + "no-augmentation" + ), + }, + ], + }, +}); diff --git a/packages/liveblocks-redux/package.json b/packages/liveblocks-redux/package.json index daf93e334fc..ab6cfee4285 100644 --- a/packages/liveblocks-redux/package.json +++ b/packages/liveblocks-redux/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/redux", - "version": "3.18.0", + "version": "3.18.1", "description": "A store enhancer to integrate Liveblocks into Redux stores. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -35,8 +35,8 @@ "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "peerDependencies": { "redux": "^4 || ^5" diff --git a/packages/liveblocks-yjs/package.json b/packages/liveblocks-yjs/package.json index 64c7f521866..4a524e97d2c 100644 --- a/packages/liveblocks-yjs/package.json +++ b/packages/liveblocks-yjs/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/yjs", - "version": "3.18.0", + "version": "3.18.1", "description": "Integrate your existing or new Yjs documents with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -35,8 +35,8 @@ "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0", + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1", "@noble/hashes": "^1.8.0", "js-base64": "^3.7.7", "y-indexeddb": "^9.0.12" diff --git a/packages/liveblocks-zustand/package.json b/packages/liveblocks-zustand/package.json index 0d862290543..bbf58d05447 100644 --- a/packages/liveblocks-zustand/package.json +++ b/packages/liveblocks-zustand/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/zustand", - "version": "3.18.0", + "version": "3.18.1", "description": "A middleware for Zustand to automatically synchronize your stores with Liveblocks. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.", "license": "Apache-2.0", "author": "Liveblocks Inc.", @@ -32,12 +32,12 @@ "lint:package": "publint --strict && attw --pack", "test": "npx liveblocks dev -p 1154 -c 'vitest run'", "test:ci": "vitest run", - "test:types": "tsd", + "test:types": "vitest run --config ./vitest.config.typecheck.ts", "test:watch": "vitest" }, "dependencies": { - "@liveblocks/client": "3.18.0", - "@liveblocks/core": "3.18.0" + "@liveblocks/client": "3.18.1", + "@liveblocks/core": "3.18.1" }, "peerDependencies": { "zustand": "^5.0.1" diff --git a/packages/liveblocks-zustand/test-d/basic.test-d.ts b/packages/liveblocks-zustand/test-d/basic.test-d.ts index e81b60152af..a5332dc2594 100644 --- a/packages/liveblocks-zustand/test-d/basic.test-d.ts +++ b/packages/liveblocks-zustand/test-d/basic.test-d.ts @@ -2,10 +2,8 @@ import { createClient, LiveList } from "@liveblocks/client"; import type { WithLiveblocks } from "@liveblocks/zustand"; import { liveblocks as liveblocksMiddleware } from "@liveblocks/zustand"; import { devtools, persist } from "zustand/middleware"; - import { create } from "zustand"; - -import { expectError, expectType } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; type MyState = { value: number; @@ -35,116 +33,177 @@ type RoomEvent = { const client = createClient({ authEndpoint: "/api/auth" }); -const useStore = create< - WithLiveblocks ->()( - devtools( - persist( - liveblocksMiddleware( - (set, get) => ({ - value: 0, - setValue: (newValue: number) => { - /** - * Inner state - */ - const state = get(); - expectType(state.value); - expectType<(newValue: number) => void>(state.setValue); - expectType(state.liveblocks.isStorageLoading); - - // Presence tests - expectType(state.liveblocks.others[0]!.presence); - expectError( - state.liveblocks.others[0]!.presence.nonexistingProperty - ); - - // UserMeta tests - expectType(state.liveblocks.others[0]!.info); - expectError(state.liveblocks.others[0]!.info.nonexistingProperty); - - expectError( - state.liveblocks.room?.broadcastEvent({ - type: "INVALID_MESSAGE", - }) - ); - - state.liveblocks.room?.getStorage().then((storage) => { - storage.root.get("todos"); - expectError(storage.root.get("unknown_key")); - }); - - return set({ value: newValue }); - }, - }), - { client, storageMapping: {}, presenceMapping: { value: true } } - ), - { - name: "liveblocks", - } - ) - ) -); - -/** - * External state - */ -const state = useStore.getState(); -expectType(state.value); -expectType<(newValue: number) => void>(state.setValue); -expectType(state.liveblocks.isStorageLoading); -expectType(state.liveblocks.others[0]!.presence); -expectError(state.liveblocks.others[0]!.presence.nonexistingProperty); -expectType(state.liveblocks.others[0]!.info); -expectError(state.liveblocks.others[0]!.info.nonexistingProperty); -expectError( - state.liveblocks.room?.broadcastEvent({ - type: "INVALID_MESSAGE", - }) -); -state.liveblocks.room?.getStorage().then((storage) => { - storage.root.get("todos"); - expectError(storage.root.get("unknown_key")); -}); +describe("WithLiveblocks middleware", () => { + test("should infer Liveblocks types inside setValue callback", () => { + const useStore = create< + WithLiveblocks + >()( + devtools( + persist( + liveblocksMiddleware( + (set, get) => ({ + value: 0, + setValue: (newValue: number) => { + const state = get(); + + expectTypeOf(state.value).toEqualTypeOf(); + expectTypeOf(state.setValue).toEqualTypeOf< + (newValue: number) => void + >(); + expectTypeOf( + state.liveblocks.isStorageLoading + ).toEqualTypeOf(); + + expectTypeOf( + state.liveblocks.others[0]!.presence + ).toEqualTypeOf(); + // @ts-expect-error - unknown presence field + state.liveblocks.others[0]!.presence.nonexistingProperty; + + expectTypeOf( + state.liveblocks.others[0]!.info + ).toEqualTypeOf(); + // @ts-expect-error - unknown UserMeta field + state.liveblocks.others[0]!.info.nonexistingProperty; + + state.liveblocks.room?.broadcastEvent({ + // @ts-expect-error - invalid room event type + type: "INVALID_MESSAGE", + }); + + state.liveblocks.room?.getStorage().then((storage) => { + storage.root.get("todos"); + // @ts-expect-error - unknown storage key + storage.root.get("unknown_key"); + }); + + return set({ value: newValue }); + }, + }), + { + client, + storageMapping: {}, + presenceMapping: { value: true }, + } + ), + { name: "liveblocks" } + ) + ) + ); -/** - * Selected state - */ -useStore((state) => { - expectType(state.value); - expectType<(newValue: number) => void>(state.setValue); - expectType(state.liveblocks.isStorageLoading); - expectType(state.liveblocks.others[0]!.presence); - expectError(state.liveblocks.others[0]!.presence.nonexistingProperty); - expectType(state.liveblocks.others[0]!.info); - expectError(state.liveblocks.others[0]!.info.nonexistingProperty); - expectError( + useStore.getState().setValue(0); + }); + + test("should expose typed external getState()", () => { + const useStore = create< + WithLiveblocks + >()( + devtools( + persist( + liveblocksMiddleware( + (set) => ({ + value: 0, + setValue: (newValue: number) => set({ value: newValue }), + }), + { + client, + storageMapping: {}, + presenceMapping: { value: true }, + } + ), + { name: "liveblocks" } + ) + ) + ); + const state = useStore.getState(); + expectTypeOf(state.value).toEqualTypeOf(); + expectTypeOf(state.setValue).toEqualTypeOf<(newValue: number) => void>(); + expectTypeOf(state.liveblocks.isStorageLoading).toEqualTypeOf(); + expectTypeOf( + state.liveblocks.others[0]!.presence + ).toEqualTypeOf(); + // @ts-expect-error - unknown presence field + state.liveblocks.others[0]!.presence.nonexistingProperty; + expectTypeOf(state.liveblocks.others[0]!.info).toEqualTypeOf(); + // @ts-expect-error - unknown UserMeta field + state.liveblocks.others[0]!.info.nonexistingProperty; state.liveblocks.room?.broadcastEvent({ + // @ts-expect-error - invalid room event type type: "INVALID_MESSAGE", - }) - ); - state.liveblocks.room?.getStorage().then((storage) => { - storage.root.get("todos"); - expectError(storage.root.get("unknown_key")); + }); + state.liveblocks.room?.getStorage().then((storage) => { + storage.root.get("todos"); + // @ts-expect-error - unknown storage key + storage.root.get("unknown_key"); + }); + }); + + test("should expose typed state in subscribe callback", () => { + const useStore = create< + WithLiveblocks + >()( + devtools( + persist( + liveblocksMiddleware( + (set) => ({ + value: 0, + setValue: (newValue: number) => set({ value: newValue }), + }), + { + client, + storageMapping: {}, + presenceMapping: { value: true }, + } + ), + { name: "liveblocks" } + ) + ) + ); + useStore.subscribe((state) => { + expectTypeOf(state.value).toEqualTypeOf(); + expectTypeOf(state.setValue).toEqualTypeOf<(newValue: number) => void>(); + expectTypeOf(state.liveblocks.isStorageLoading).toEqualTypeOf(); + expectTypeOf( + state.liveblocks.others[0]!.presence + ).toEqualTypeOf(); + // @ts-expect-error - unknown presence field + state.liveblocks.others[0]!.presence.nonexistingProperty; + expectTypeOf(state.liveblocks.others[0]!.info).toEqualTypeOf(); + // @ts-expect-error - unknown UserMeta field + state.liveblocks.others[0]!.info.nonexistingProperty; + state.liveblocks.room?.broadcastEvent({ + // @ts-expect-error - invalid room event type + type: "INVALID_MESSAGE", + }); + state.liveblocks.room?.getStorage().then((storage) => { + storage.root.get("todos"); + // @ts-expect-error - unknown storage key + storage.root.get("unknown_key"); + }); + }); }); - return state; -}); -// Invalid configuration -expectError( - create>()( - devtools( - persist( - liveblocksMiddleware( - (set) => ({ - value: 0, - setValue: (newValue: number) => set({ value: newValue }), - }), - { client, storageMapping: {}, presenceMapping: { unknownKey: true } } - ), - { - name: "liveblocks", - } + test("should reject invalid presenceMapping keys", () => { + create>()( + devtools( + persist( + liveblocksMiddleware( + (set) => ({ + value: 0, + setValue: (newValue: number) => set({ value: newValue }), + }), + { + client, + storageMapping: {}, + // @ts-expect-error - unknown presence key + presenceMapping: { unknownKey: true }, + } + ), + { + name: "liveblocks", + } + ) ) - ) - ) -); + ); + }); +}); diff --git a/packages/liveblocks-zustand/test-d/slices.test-d.ts b/packages/liveblocks-zustand/test-d/slices.test-d.ts index b8a5fd4ca2e..58f8454e35c 100644 --- a/packages/liveblocks-zustand/test-d/slices.test-d.ts +++ b/packages/liveblocks-zustand/test-d/slices.test-d.ts @@ -4,8 +4,7 @@ import { subscribeWithSelector } from "zustand/middleware"; import { createClient } from "@liveblocks/client"; import { liveblocks as liveblocksMiddleware } from "@liveblocks/zustand"; import type { WithLiveblocks } from "@liveblocks/zustand"; - -import { expectType, expectAssignable } from "tsd"; +import { describe, expectTypeOf, test } from "vitest"; type BearSlice = { bears: number; @@ -63,27 +62,33 @@ const useStore = create>()( ) ); -const fullstate = useStore((s) => s); +describe("WithLiveblocks middleware with slices", () => { + test("should expose slice fields and Liveblocks API from getState()", () => { + const fullstate = useStore.getState(); -// From fish slice -expectType(fullstate.fishes); -expectType<() => void>(fullstate.addFish); + // From fish slice + expectTypeOf(fullstate.fishes).toEqualTypeOf(); + expectTypeOf(fullstate.addFish).toEqualTypeOf<() => void>(); -// From bear slice -expectType<() => void>(fullstate.eatFish); + // From bear slice + expectTypeOf(fullstate.eatFish).toEqualTypeOf<() => void>(); -// Liveblocks state -expectAssignable(fullstate.liveblocks.enterRoom); -expectAssignable(fullstate.liveblocks.leaveRoom); -expectType(fullstate.liveblocks.room!.id); + // Liveblocks state + expectTypeOf(fullstate.liveblocks.enterRoom).toExtend< + (roomId: string, options?: object) => () => void + >(); + expectTypeOf(fullstate.liveblocks.leaveRoom).toEqualTypeOf<() => void>(); + expectTypeOf(fullstate.liveblocks.room!.id).toEqualTypeOf(); + }); -// Test subscribe with selector middleware -expectType<() => void>( - useStore.subscribe( - (state) => state.bears, - (n) => { - expectType(n); - console.log(n); - } - ) -); + test("should return typed state in subscribe callback", () => { + expectTypeOf( + useStore.subscribe( + (state) => state.bears, + (bears) => { + expectTypeOf(bears).toEqualTypeOf(); + } + ) + ).toEqualTypeOf<() => void>(); + }); +}); diff --git a/packages/liveblocks-zustand/vitest.config.typecheck.ts b/packages/liveblocks-zustand/vitest.config.typecheck.ts new file mode 100644 index 00000000000..8bdcb63a0e0 --- /dev/null +++ b/packages/liveblocks-zustand/vitest.config.typecheck.ts @@ -0,0 +1,6 @@ +import { makeTypecheckTestConfig } from "@liveblocks/vitest-config/typecheck"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: makeTypecheckTestConfig(import.meta, ["test-d/**/*.test-d.ts"]), +}); diff --git a/shared/vitest-config/package.json b/shared/vitest-config/package.json index a3da9165814..82ff07c5056 100644 --- a/shared/vitest-config/package.json +++ b/shared/vitest-config/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "exports": { - ".": "./index.js" + ".": "./index.js", + "./typecheck": "./typecheck.js" }, "dependencies": { "fast-check": "^4.3.0", diff --git a/shared/vitest-config/typecheck.js b/shared/vitest-config/typecheck.js new file mode 100644 index 00000000000..113568d8dde --- /dev/null +++ b/shared/vitest-config/typecheck.js @@ -0,0 +1,74 @@ +import { mkdirSync, writeFileSync } from "fs"; +import { basename, dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); +const TMP_DIR = "liveblocks-typecheck"; + +/** + * @param {string} name + * @returns {string} + */ +function slugify(name) { + let slug = String(name ?? "") + .replace(/[/\\]/g, "-") + .replace(/[<>:"|?*]/g, "-") + .replace(/\s+/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/^[.\s]+|[.\s]+$/g, ""); + + if (!slug) { + slug = "."; + } + + return slug; +} + +/** + * Prepare a Vitest config for typechecking test-d files. It writes a + * temporary TS config that extends the package's `tsconfig.json`. + * + * @param {ImportMeta} importMeta Import meta object (e.g. `import.meta`). + * @param {string[]} testFiles Test files to typecheck, relative to the package root. + * @param {string | undefined} [name] Name of the test group. + * @returns {import("vitest/node").InlineConfig} + */ +export function makeTypecheckTestConfig(importMeta, testFiles, name) { + const pkgDir = fileURLToPath(new URL(".", importMeta.url)); + const tmpDirPath = join( + MONOREPO_ROOT, + "node_modules", + ".cache", + TMP_DIR, + basename(pkgDir) + ); + const tmpTsConfigPath = join(tmpDirPath, `${slugify(name)}.json`); + const absoluteTestFiles = testFiles.map((file) => join(pkgDir, file)); + + // Vitest's `typecheck` needs a TS config to work, we generate a temporary one + // automatically to avoid having to keep them at the root and duplicating `include` paths. + mkdirSync(tmpDirPath, { recursive: true }); + writeFileSync( + tmpTsConfigPath, + JSON.stringify({ + extends: join(pkgDir, "tsconfig.json"), + compilerOptions: { + noUnusedLocals: false, + noUnusedParameters: false, + }, + include: [join(pkgDir, "src"), ...absoluteTestFiles], + }) + ); + + return { + name, + typecheck: { + enabled: true, + only: true, + ignoreSourceErrors: true, + tsconfig: tmpTsConfigPath, + include: absoluteTestFiles, + }, + }; +} diff --git a/turbo.json b/turbo.json index a42a909cb50..509b10ee0bf 100644 --- a/turbo.json +++ b/turbo.json @@ -30,7 +30,7 @@ }, "test:types": { "dependsOn": ["build"], - "inputs": ["test-d/**"] + "inputs": ["test-d/**", "vitest.*"] }, "test:deps": { // A package's `test:deps` script has no dependencies and can be run