├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── eslint.config.js
├── logo.png
├── logo.svg
├── package-lock.json
├── package.json
├── release.config.cjs
├── src
├── app.d.ts
├── app.html
├── lib
│ ├── common.ts
│ ├── components
│ │ ├── Record.svelte
│ │ ├── Record.svelte.test.ts
│ │ ├── Records.svelte
│ │ └── Records.svelte.test.ts
│ ├── index.ts
│ ├── stores.svelte.test.ts
│ └── stores.svelte.ts
└── routes
│ ├── +page.svelte
│ └── +page.ts
├── static
└── favicon.png
├── svelte.config.js
├── tests
├── pb_schema.json
├── vitest-pocketbase-setup.d.ts
└── vitest-pocketbase-setup.ts
├── tsconfig.json
├── vite.config.ts
└── vitest-setup-client.ts
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm"
4 | directory: "/"
5 | commit-message:
6 | prefix: build
7 | include: scope
8 | schedule:
9 | interval: "weekly"
10 | groups:
11 | non-major:
12 | update-types:
13 | - "minor"
14 | - "patch"
15 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ main ]
7 | pull_request:
8 | branches: [ main ]
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 | with:
18 | persist-credentials: false
19 |
20 | - name: Setup Node.js
21 | uses: actions/setup-node@v4
22 | with:
23 | node-version: '20'
24 | cache: 'npm'
25 |
26 | - name: Install dependencies
27 | run: npm ci
28 |
29 | - name: Download and extract PocketBase
30 | run: |
31 | wget https://github.com/pocketbase/pocketbase/releases/download/v0.25.8/pocketbase_0.25.8_linux_amd64.zip
32 | unzip pocketbase_0.25.8_linux_amd64.zip
33 |
34 | - name: Start PocketBase in background
35 | run: |
36 | ./pocketbase serve &
37 | # Wait for PocketBase to start
38 | sleep 2
39 | ./pocketbase superuser upsert pocketbase@svelte.com pocketbase
40 |
41 | - name: Run tests
42 | run: npm test
43 |
44 | - name: Semantic Release
45 | uses: cycjimmy/semantic-release-action@v4
46 | with:
47 | branches: main
48 | env:
49 | GH_TOKEN: ${{ secrets.GH_TOKEN }}
50 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | .netlify
7 | .wrangler
8 | /.svelte-kit
9 | /build
10 | /dist
11 |
12 | # OS
13 | .DS_Store
14 | Thumbs.db
15 |
16 | # Env
17 | .env
18 | .env.*
19 | !.env.example
20 | !.env.test
21 |
22 | # Vite
23 | vite.config.js.timestamp-*
24 | vite.config.ts.timestamp-*
25 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Max Shipit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SveltePocket
2 |
3 | 
4 |
5 | Svelte 5-ready stores and components to bring data from any Pocketbase instance into your Svelte application (even with realtime updates 🤫).
6 |
7 | ## Installation
8 |
9 | ```bash
10 | npm install @shipitdev/sveltepocket
11 | ```
12 |
13 | ## Setup
14 |
15 | Call the init method to pass your Pocketbase client SDK instance. This will be used by all stores and components so make sure it's authenticated according to your needs.
16 |
17 | ```svelte
18 |
25 | ```
26 |
27 | ## Stores
28 |
29 | The stores provide an low level API to query data from your Pocketbase instance and are the best choice when you need to pre/postprocess the data instead of just rendering it onto the page.
30 |
31 | ### auth
32 |
33 | A readable store that holds the current user's authentication status and user record.
34 |
35 | ```svelte
36 |
39 |
40 | {#if $auth.isAuthenticated}
41 |
Welcome, {$auth.user.email}!
42 | {/if}
43 | ```
44 |
45 | ### createRecordStore
46 |
47 | Creates a readable store that fetches a single record identified by id or filter from a Pocketbase collection.
48 |
49 | ```svelte
50 |
55 |
56 | {#if $post.record}
57 | {$post.record.title}
58 | {/if}
59 | ```
60 |
61 | ### createRecordsStore
62 |
63 | Creates a readable store that fetches multiple records from a Pocketbase collection.
64 |
65 | ```svelte
66 |
71 |
72 | {#if $posts.records}
73 |
74 | {#each $posts.records as post}
75 | {post.title}
76 | {/each}
77 |
78 | {/if}
79 | ```
80 |
81 | ## Components
82 |
83 | If you only care about rendering the queried data, these components are the way to go.
84 |
85 | You can pass snippets that will be rendered during different states, e.g. when loading the data, when data is not found or when an error occurs.
86 |
87 | ### \
88 |
89 | A component that fetches a single record either by ID or filter from a Pocketbase collection and renders it.
90 |
91 | ```svelte
92 |
93 |
94 | {#snippet render(post)}
95 | {post.title}
96 | by {post.expand.author.name}
97 | {/snippet}
98 |
99 |
100 |
101 |
102 | {#snippet render(post)}
103 | {post.title}
104 | {/snippet}
105 |
106 | ```
107 |
108 | ### \
109 |
110 | A component that fetches multiple records from a Pocketbase collection and renders them.
111 |
112 | ```svelte
113 |
114 | {#snippet render(posts)}
115 |
116 | {#each posts as post}
117 | {post.title} by {post.expand.author.name}
118 | {/each}
119 |
120 | {/snippet}
121 |
122 | ```
123 |
124 | ## Realtime Updates
125 |
126 | All stores and components support the `realtime` parameter. If set to `true`, SveltePocket will setup a subscription to PocketBase's realtime updates and keep the data up to date.
127 |
128 | Combined with Svelte's reactivity, your app will rerender automatically when the data changes.
129 |
130 | ```svelte
131 |
132 |
133 | {#snippet render(posts)}
134 |
135 | {#each posts as post}
136 | {post.title}
137 | {/each}
138 |
139 | {/snippet}
140 |
141 | ```
142 |
143 | ## Type Safety
144 |
145 | All stores and components take an optional record type, e.g. generated by [pocketbase-typegen](https://github.com/patmood/pocketbase-typegen).
146 | This gives you full type safety on the returned records.
147 |
148 | ### Store
149 |
150 | ```svelte
151 |
157 | ```
158 |
159 | ### Components
160 |
161 | ```svelte
162 |
163 | {#snippet render(records: PostRecord[])}
164 | ...
165 | {/snippet}
166 |
167 | ```
168 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from 'eslint-config-prettier';
2 | import js from '@eslint/js';
3 | import { includeIgnoreFile } from '@eslint/compat';
4 | import svelte from 'eslint-plugin-svelte';
5 | import globals from 'globals';
6 | import { fileURLToPath } from 'node:url';
7 | import ts from 'typescript-eslint';
8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
9 |
10 | export default ts.config(
11 | includeIgnoreFile(gitignorePath),
12 | js.configs.recommended,
13 | ...ts.configs.recommended,
14 | ...svelte.configs['flat/recommended'],
15 | prettier,
16 | ...svelte.configs['flat/prettier'],
17 | {
18 | languageOptions: {
19 | globals: {
20 | ...globals.browser,
21 | ...globals.node
22 | }
23 | }
24 | },
25 | {
26 | files: ['**/*.svelte'],
27 |
28 | languageOptions: {
29 | parserOptions: {
30 | parser: ts.parser
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brennerm/sveltepocket/156a7938dfba30454f34f922769a1ece876082b4/logo.png
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
483 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shipitdev/sveltepocket",
3 | "description": "Svelte stores and components to query data from PocketBase",
4 | "version": "0.0.3",
5 | "license": "MIT",
6 | "keywords": [
7 | "svelte",
8 | "pocketbase"
9 | ],
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/brennerm/sveltepocket.git"
13 | },
14 | "scripts": {
15 | "dev": "vite dev",
16 | "build": "vite build && npm run prepack",
17 | "preview": "vite preview",
18 | "prepare": "svelte-kit sync || echo ''",
19 | "prepack": "svelte-kit sync && svelte-package && publint",
20 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
21 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
22 | "format": "prettier --write .",
23 | "lint": "prettier --check . && eslint .",
24 | "test:unit": "vitest --no-file-parallelism",
25 | "test": "npm run test:unit -- --run"
26 | },
27 | "files": [
28 | "dist",
29 | "!dist/**/*.test.*",
30 | "!dist/**/*.spec.*"
31 | ],
32 | "sideEffects": [
33 | "**/*.css"
34 | ],
35 | "svelte": "./dist/index.js",
36 | "types": "./dist/index.d.ts",
37 | "type": "module",
38 | "exports": {
39 | ".": {
40 | "types": "./dist/index.d.ts",
41 | "svelte": "./dist/index.js"
42 | }
43 | },
44 | "peerDependencies": {
45 | "svelte": "^5.0.0"
46 | },
47 | "devDependencies": {
48 | "@eslint/compat": "^1.2.5",
49 | "@eslint/js": "^9.18.0",
50 | "@sveltejs/adapter-auto": "^4.0.0",
51 | "@sveltejs/kit": "^2.16.0",
52 | "@sveltejs/package": "^2.0.0",
53 | "@sveltejs/vite-plugin-svelte": "^5.0.0",
54 | "@testing-library/jest-dom": "^6.6.3",
55 | "@testing-library/svelte": "^5.2.7",
56 | "eslint": "^9.18.0",
57 | "eslint-config-prettier": "^10.0.1",
58 | "eslint-plugin-svelte": "^3.0.2",
59 | "globals": "^16.0.0",
60 | "jsdom": "^26.0.0",
61 | "pocketbase": "^0.26.0",
62 | "prettier": "^3.4.2",
63 | "prettier-plugin-svelte": "^3.3.3",
64 | "publint": "^0.3.2",
65 | "svelte": "^5.0.0",
66 | "svelte-check": "^4.0.0",
67 | "typescript": "^5.0.0",
68 | "typescript-eslint": "^8.20.0",
69 | "vite": "^6.0.0",
70 | "vitest": "^3.0.0"
71 | }
72 | }
--------------------------------------------------------------------------------
/release.config.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('semantic-release').GlobalConfig}
3 | */
4 | module.exports = {
5 | branches: ["main"],
6 | plugins: [
7 | [
8 | "@semantic-release/commit-analyzer",
9 | {
10 | "preset": "angular",
11 | "releaseRules": [
12 | { "type": "build", "release": "patch" }
13 | ],
14 | }
15 | ],
16 | "@semantic-release/release-notes-generator",
17 | "@semantic-release/github",
18 | "@semantic-release/npm"
19 | ],
20 | prefix: "angular"
21 | };
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export { };
14 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/common.ts:
--------------------------------------------------------------------------------
1 | import type { ClientResponseError } from 'pocketbase';
2 | import type { Snippet } from 'svelte';
3 |
4 | export type CommonParams = {
5 | /** the collection to fetch records from */
6 | collection: string;
7 | /** the filter to apply, e.g. "status = 'published'" */
8 | filter?: string;
9 | /** fields to expand, e.g "author,comments" */
10 | expand?: string;
11 | /** sort expression, e.g. "-created" */
12 | sort?: string;
13 | /** whether to subscribe to realtime updates */
14 | realtime?: boolean;
15 | /** the snippet to show while loading the data from Pocketbase */
16 | loading?: Snippet;
17 | /** the snippet to show when an error occurs */
18 | error?: Snippet<[error: ClientResponseError]>;
19 | /** the snippet to show when no record is found or the collection does not exist */
20 | notFound?: Snippet;
21 | };
22 |
--------------------------------------------------------------------------------
/src/lib/components/Record.svelte:
--------------------------------------------------------------------------------
1 |
22 |
23 |
48 |
49 | {JSON.stringify($store)}
50 |
51 | {#if $store.isLoading}
52 | {@render loading?.()}
53 | {:else if $store.error}
54 | {@render error?.($store.error)}
55 | {:else if $store.record === null}
56 | {@render notFound?.()}
57 | {:else if $store.record}
58 | {@render render?.($store.record)}
59 | {/if}
60 |
--------------------------------------------------------------------------------
/src/lib/components/Record.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/svelte';
2 | import { describe, expect } from 'vitest';
3 | import Record from './Record.svelte';
4 | import { AUTHORS, pbtest, POSTS } from '../../../tests/vitest-pocketbase-setup.js';
5 | import { createRawSnippet } from 'svelte';
6 |
7 | describe.sequential('Record.svelte', () => {
8 | pbtest('loading', async () => {
9 | const { baseElement } = render(Record, {
10 | collection: 'posts', id: '000000000000000',
11 | loading: createRawSnippet(() => ({
12 | render: () => 'Loading... '
13 | }))
14 | });
15 | expect(baseElement).toHaveTextContent('Loading...');
16 | });
17 |
18 | pbtest('notFound', async () => {
19 | const { baseElement } = render(Record, {
20 | collection: 'posts', id: 'nonexistent',
21 | notFound: createRawSnippet(() => ({
22 | render: () => 'Not Found '
23 | }))
24 | });
25 |
26 | await screen.findByText('Not Found');
27 | expect(baseElement).toHaveTextContent('Not Found');
28 | });
29 |
30 | pbtest('error', async ({ pb }) => {
31 | pb.authStore.clear();
32 |
33 | const { baseElement } = render(Record, {
34 | collection: 'posts', id: '000000000000000',
35 | error: createRawSnippet((error) => ({
36 | render: () => `Error ${error().response.status} `
37 | }))
38 | });
39 |
40 | await screen.findByText('Error 403');
41 | expect(baseElement).toHaveTextContent('Error 403');
42 | });
43 |
44 | pbtest('id', async () => {
45 | const { baseElement } = render(Record, {
46 | collection: 'posts', id: '000000000000000',
47 | render: createRawSnippet((record) => ({
48 | render: () => `${record().title} `
49 | })),
50 | loading: createRawSnippet(() => ({
51 | render: () => 'Loading... '
52 | }))
53 | });
54 |
55 | await screen.findByRole('heading');
56 | expect(baseElement).toHaveTextContent(POSTS[0].title);
57 | expect(baseElement).not.toHaveTextContent('Loading...');
58 | });
59 |
60 | pbtest('filter', async () => {
61 | const { baseElement } = render(Record, {
62 | collection: 'posts', filter: 'published = false',
63 | render: createRawSnippet((record) => ({
64 | render: () => `${record().title} `
65 | })),
66 | });
67 |
68 | await screen.findByRole('heading');
69 | expect(baseElement).toHaveTextContent(POSTS[1].title);
70 | });
71 |
72 | pbtest('expand', async () => {
73 | const { baseElement } = render(Record, {
74 | collection: 'posts', id: '000000000000000', expand: 'author',
75 | render: createRawSnippet((record) => ({
76 | render: () => `${record().expand.author.name} `
77 | })),
78 | });
79 |
80 | await screen.findByRole('heading');
81 | expect(baseElement).toHaveTextContent(AUTHORS[0].name);
82 | });
83 | });
--------------------------------------------------------------------------------
/src/lib/components/Records.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
55 |
56 | {#if $store.isLoading}
57 | {@render loading?.()}
58 | {:else if $store.error}
59 | {@render error?.($store.error)}
60 | {:else if $store.records === null}
61 | {@render notFound?.()}
62 | {:else if $store.records}
63 | {@render render?.($store.records, $store.totalPages, $store.totalItems)}
64 | {/if}
65 |
--------------------------------------------------------------------------------
/src/lib/components/Records.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/svelte';
2 | import { describe, expect } from 'vitest';
3 | import Records from './Records.svelte';
4 | import { AUTHORS, pbtest, POSTS } from '../../../tests/vitest-pocketbase-setup.js';
5 | import { createRawSnippet } from 'svelte';
6 |
7 | describe.sequential('Records.svelte', () => {
8 | pbtest('loading', async () => {
9 | const { baseElement } = render(Records, {
10 | collection: 'posts',
11 | loading: createRawSnippet(() => ({
12 | render: () => 'Loading... '
13 | }))
14 | });
15 | expect(baseElement).toHaveTextContent('Loading...');
16 | });
17 |
18 | pbtest('notFound', async () => {
19 | const { baseElement } = render(Records, {
20 | collection: 'nonexistent',
21 | notFound: createRawSnippet(() => ({
22 | render: () => 'Not Found '
23 | }))
24 | });
25 |
26 | await screen.findByText('Not Found');
27 | expect(baseElement).toHaveTextContent('Not Found');
28 | });
29 |
30 | pbtest('error', async ({ pb }) => {
31 | pb.authStore.clear();
32 |
33 | const { baseElement } = render(Records, {
34 | collection: 'posts',
35 | error: createRawSnippet((error) => ({
36 | render: () => `Error ${error().response.status} `
37 | }))
38 | });
39 |
40 | await screen.findByText('Error 403');
41 | expect(baseElement).toHaveTextContent('Error 403');
42 | });
43 |
44 | pbtest('render', async () => {
45 | const { baseElement } = render(Records, {
46 | collection: 'posts',
47 | render: createRawSnippet((records) => ({
48 | render: () => `${records().map((r) => r.title).join(',')} `
49 | })),
50 | loading: createRawSnippet(() => ({
51 | render: () => 'Loading... '
52 | }))
53 | });
54 |
55 | await screen.findByRole('heading');
56 | expect(baseElement).toHaveTextContent(POSTS.map((r) => r.title).join(','));
57 | expect(baseElement).not.toHaveTextContent('Loading...');
58 | });
59 |
60 | pbtest('sort', async () => {
61 | const { baseElement } = render(Records, {
62 | collection: 'posts', sort: '-id',
63 | render: createRawSnippet((records) => ({
64 | render: () => `${records().map((r) => r.title).join(',')} `
65 | })),
66 | });
67 |
68 | await screen.findByRole('heading');
69 | expect(baseElement).toHaveTextContent(POSTS.sort((a, b) => b.id - a.id).map((r) => r.title).join(','));
70 | });
71 |
72 | pbtest('filter', async () => {
73 | const { baseElement } = render(Records, {
74 | collection: 'posts', filter: 'published = false',
75 | render: createRawSnippet((records) => ({
76 | render: () => `${records().map((r) => r.title).join(',')} `
77 | })),
78 | });
79 |
80 | await screen.findByRole('heading');
81 | expect(baseElement).toHaveTextContent(POSTS.filter((r) => !r.published).map((r) => r.title).join(','));
82 | });
83 |
84 | pbtest('expand', async () => {
85 | const { baseElement } = render(Records, {
86 | collection: 'posts', id: '000000000000000', expand: 'author',
87 | render: createRawSnippet((records) => ({
88 | render: () => `${records().map((r) => r.expand.author.name).join(',')} `
89 | })),
90 | });
91 |
92 | await screen.findByRole('heading');
93 | expect(baseElement).toHaveTextContent(POSTS.map((p) => AUTHORS.find((a) => a.id === p.author).name).join(','));
94 | });
95 | });
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { auth, init, createRecordStore, createRecordsStore } from "./stores.svelte.js";
2 | import Record from "./components/Record.svelte";
3 | import Records from "./components/Records.svelte";
4 |
5 | export { auth, init, createRecordStore, createRecordsStore, Record, Records };
6 |
--------------------------------------------------------------------------------
/src/lib/stores.svelte.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, vi } from 'vitest';
2 | import { createRecordStore, createRecordsStore, auth } from './stores.svelte.js';
3 | import { get } from 'svelte/store';
4 | import { AUTHORS, pbtest, POSTS, USERS } from '../../tests/vitest-pocketbase-setup.js';
5 |
6 | describe.sequential('record store', () => {
7 | pbtest('non-existing collection', async () => {
8 | const store = createRecordStore('non-existing-collection', { id: 'id' });
9 | store.subscribe(() => { })
10 |
11 | await vi.waitUntil(() => get(store).record !== undefined)
12 | expect(get(store).record).toBe(null);
13 | });
14 |
15 | pbtest('non-existing id', async () => {
16 | const store = createRecordStore('posts', { id: 'non-existing-id' });
17 | store.subscribe(() => { })
18 |
19 | await vi.waitUntil(() => get(store).record !== undefined)
20 | expect(get(store).record).toBe(null);
21 | })
22 |
23 | pbtest('id', async () => {
24 | const store = createRecordStore('posts', { id: '000000000000000' });
25 | store.subscribe(() => { })
26 |
27 | await vi.waitUntil(() => get(store).record !== undefined)
28 | const record = get(store).record;
29 | expect(record.id).toBe(POSTS[0].id);
30 | expect(record.title).toBe(POSTS[0].title);
31 | expect(record.published).toBe(POSTS[0].published);
32 | })
33 |
34 | pbtest('filter', async () => {
35 | const store = createRecordStore('posts', { filter: 'published = false' });
36 | store.subscribe(() => { })
37 |
38 | await vi.waitUntil(() => get(store).record !== undefined)
39 | const record = get(store).record;
40 | expect(record.id).toBe(POSTS[1].id);
41 | expect(record.title).toBe(POSTS[1].title);
42 | expect(record.published).toBe(POSTS[1].published);
43 | })
44 |
45 | pbtest('expand', async () => {
46 | const store = createRecordStore('posts', { id: '000000000000000', expand: 'author' });
47 | store.subscribe(() => { })
48 |
49 | await vi.waitUntil(() => get(store).record !== undefined)
50 | const record = get(store).record;
51 | expect(record.id).toBe(POSTS[0].id);
52 | expect(record.title).toBe(POSTS[0].title);
53 | expect(record.published).toBe(POSTS[0].published);
54 | expect(record.expand.author.id).toBe(AUTHORS[0].id);
55 | expect(record.expand.author.name).toBe(AUTHORS[0].name);
56 | })
57 |
58 | pbtest('realtime', async ({ pb, skip }) => {
59 | skip('Realtime test is skipped because we need to find a way to mock the EventSource object');
60 |
61 | const store = createRecordStore('posts', { id: '000000000000000', realtime: true });
62 | store.subscribe(() => { })
63 |
64 | await vi.waitUntil(() => get(store).record !== undefined)
65 | const record = get(store).record;
66 | expect(record.id).toBe(POSTS[0].id);
67 | expect(record.title).toBe(POSTS[0].title);
68 | expect(record.published).toBe(POSTS[0].published);
69 |
70 | await pb.collection('posts').update(POSTS[0].id, { title: 'New Title' });
71 |
72 | await vi.waitUntil(() => get(store).record.title === 'New Title');
73 | })
74 | });
75 |
76 | describe.sequential('records store', () => {
77 | pbtest('non-existing collection', async () => {
78 | const store = createRecordsStore('non-existing-collection');
79 | store.subscribe(() => { })
80 | await vi.waitUntil(() => get(store).records !== undefined)
81 |
82 | expect(get(store).records).toBe(null);
83 | });
84 |
85 | pbtest('existing collection', async () => {
86 | const store = createRecordsStore('posts');
87 | store.subscribe(() => { })
88 | await vi.waitUntil(() => get(store).records !== undefined)
89 |
90 | const records = get(store).records;
91 | expect(records.length).toBe(POSTS.length);
92 | records.forEach((record, i) => {
93 | expect(record.id).toBe(POSTS[i].id);
94 | expect(record.title).toBe(POSTS[i].title);
95 | expect(record.published).toBe(POSTS[i].published);
96 | })
97 | })
98 |
99 | pbtest('sort', async () => {
100 | const store = createRecordsStore('posts', { sort: '-id' });
101 | store.subscribe(() => { })
102 | await vi.waitUntil(() => get(store).records !== undefined)
103 |
104 | const records = get(store).records;
105 | expect(records.length).toBe(POSTS.length);
106 | records.forEach((record, i) => {
107 | expect(record.id).toBe(POSTS[9 - i].id);
108 | expect(record.title).toBe(POSTS[9 - i].title);
109 | expect(record.published).toBe(POSTS[9 - i].published);
110 | })
111 | })
112 |
113 | pbtest('expand', async () => {
114 | const store = createRecordsStore('posts', { expand: 'author' });
115 | store.subscribe(() => { })
116 | await vi.waitUntil(() => get(store).records !== undefined)
117 |
118 | const records = get(store).records;
119 | expect(records.length).toBe(POSTS.length);
120 | records.forEach((record, i) => {
121 | expect(record.id).toBe(POSTS[i].id);
122 | expect(record.title).toBe(POSTS[i].title);
123 | expect(record.published).toBe(POSTS[i].published);
124 | expect(record.expand.author.name).not.toBe(undefined);
125 | })
126 | })
127 |
128 | pbtest('filter', async () => {
129 | const store = createRecordsStore('posts', { filter: 'published = false' });
130 | store.subscribe(() => { })
131 | await vi.waitUntil(() => get(store).records !== undefined)
132 |
133 | const privatePosts = POSTS.filter(post => !post.published);
134 |
135 | const records = get(store).records;
136 | expect(records.length).toBe(privatePosts.length);
137 | records.forEach((record, i) => {
138 | expect(record.id).toBe(privatePosts[i].id);
139 | expect(record.title).toBe(privatePosts[i].title);
140 | expect(record.published).toBe(privatePosts[i].published);
141 | })
142 | })
143 | });
144 |
145 | describe.sequential('auth', () => {
146 | pbtest('superuser', async () => {
147 | auth.subscribe(() => { });
148 |
149 | // we already should be authenticated as a superuser
150 | expect(get(auth).isAuthenticated).toBe(true);
151 | expect(get(auth).isSuperuser).toBe(true);
152 | });
153 |
154 | pbtest('login', async ({ pb }) => {
155 | auth.subscribe(() => { });
156 |
157 | pb.authStore.clear();
158 | expect(get(auth).isAuthenticated).toBe(false);
159 |
160 | await pb.collection('users').authWithPassword(USERS[0].email, USERS[0].password);
161 |
162 | vi.waitUntil(() => get(auth).isAuthenticated);
163 | expect(get(auth).isSuperuser).toBe(false);
164 | expect(get(auth).user.email).toBe(USERS[0].email);
165 | });
166 |
167 | pbtest('logout', async ({ pb }) => {
168 | auth.subscribe(() => { });
169 |
170 | expect(get(auth).isAuthenticated).toBe(true);
171 | pb.authStore.clear();
172 | expect(get(auth).isAuthenticated).toBe(false);
173 | });
174 | });
--------------------------------------------------------------------------------
/src/lib/stores.svelte.ts:
--------------------------------------------------------------------------------
1 | import Pocketbase from 'pocketbase';
2 | import { readable } from 'svelte/store';
3 | import type {
4 | RecordModel,
5 | RecordListOptions,
6 | RecordSubscribeOptions,
7 | ClientResponseError,
8 | AuthRecord
9 | } from 'pocketbase';
10 | import { BROWSER } from 'esm-env'
11 |
12 | export const PB = () => {
13 | return _pb;
14 | };
15 |
16 | let _pb = $state(null);
17 |
18 | export const init = (value: Pocketbase) => {
19 | _pb = value;
20 | };
21 |
22 | /** a Svelte store that holds the current user's authentication status and user record */
23 | export const auth = readable<{
24 | isAuthenticated: boolean | undefined;
25 | isSuperuser: boolean | undefined;
26 | user: AuthRecord | undefined;
27 | }>({ isAuthenticated: undefined, isSuperuser: undefined, user: undefined }, (set) => {
28 | const unsubscribe = _pb?.authStore.onChange((_, record) => {
29 | set({
30 | isAuthenticated: _pb?.authStore.isValid,
31 | isSuperuser: _pb?.authStore.isSuperuser,
32 | user: record
33 | });
34 | }, true);
35 |
36 | return unsubscribe;
37 | });
38 |
39 | type RecordStoreOptions = {
40 | id?: string;
41 | filter?: string;
42 | expand?: string;
43 | realtime?: boolean;
44 | };
45 |
46 | /** create a Svelte store that fetches a single record identified by id or filter from a Pocketbase collection */
47 | export const createRecordStore = (
48 | collection: string,
49 | { id, filter, expand, realtime }: RecordStoreOptions = {}
50 | ) => {
51 | return readable<{
52 | record: T | undefined | null;
53 | isLoading: boolean;
54 | error: ClientResponseError | null;
55 | }>(
56 | {
57 | record: undefined,
58 | isLoading: false,
59 | error: null
60 | },
61 | (_, update) => {
62 | let unsubscribe = () => { };
63 |
64 | const subscribe = (recordId: string) => {
65 | if (!BROWSER || !realtime) return;
66 |
67 | _pb
68 | ?.collection(collection)
69 | .subscribe(recordId, ({ action, record }) => {
70 | switch (action) {
71 | case 'update':
72 | update((data) => ({ ...data, record: record as unknown as T }));
73 | break;
74 | case 'delete':
75 | update((data) => ({ ...data, record: null }));
76 | break;
77 | }
78 | }, { expand })
79 | .then((value) => {
80 | unsubscribe = value;
81 | });
82 | };
83 |
84 | const promise = id
85 | ? _pb?.collection(collection).getOne(id, { expand })
86 | : filter
87 | ? _pb?.collection(collection).getFirstListItem(filter, { expand })
88 | : null;
89 |
90 | if (promise) {
91 | update((data) => ({ ...data, isLoading: true }));
92 | promise
93 | .then((value) => {
94 | update((data) => ({ ...data, record: value as T }));
95 | subscribe(value.id);
96 | })
97 | .catch((reason) => {
98 | if (reason.response.status === 404) {
99 | update((data) => ({ ...data, record: null }));
100 | } else {
101 | update((data) => ({ ...data, error: reason }));
102 | }
103 | })
104 | .finally(() => {
105 | update((data) => ({ ...data, isLoading: false }));
106 | });
107 | }
108 |
109 | return () => {
110 | unsubscribe();
111 | unsubscribe = () => { };
112 | };
113 | }
114 | );
115 | };
116 |
117 | type RecordsStoreOptions = {
118 | sort?: string;
119 | expand?: string;
120 | filter?: string;
121 | realtime?: boolean;
122 | listOptions?: RecordListOptions;
123 | subscribeOptions?: RecordSubscribeOptions;
124 | };
125 |
126 | /** create a Svelte store that fetches multiple records from a Pocketbase collection */
127 | export const createRecordsStore = (
128 | collection: string,
129 | { sort, expand, filter, realtime, listOptions, subscribeOptions }: RecordsStoreOptions = {}
130 | ) => {
131 | return readable<{
132 | records: T[] | undefined | null;
133 | totalPages: number;
134 | totalItems: number;
135 | isLoading: boolean;
136 | error: ClientResponseError | null;
137 | }>(
138 | {
139 | records: undefined,
140 | totalPages: 0,
141 | totalItems: 0,
142 | isLoading: false,
143 | error: null
144 | },
145 | (_, update) => {
146 | let unsubscribe = () => { };
147 |
148 | const subscribe = () => {
149 | if (!BROWSER || !realtime) return;
150 |
151 | _pb
152 | ?.collection(collection)
153 | .subscribe(
154 | '*',
155 | ({ action, record }) => {
156 | update((data) => {
157 | if (!data.records) return data;
158 |
159 | switch (action) {
160 | case 'create':
161 | return {
162 | ...data,
163 | records: [record as T, ...data.records]
164 | };
165 | case 'update':
166 | return {
167 | ...data,
168 | records: data.records.map(
169 | (item) => (item.id === record.id ? record : item) as T
170 | )
171 | };
172 | case 'delete':
173 | return {
174 | ...data,
175 | records: data.records.filter((item) => item.id !== record.id)
176 | };
177 | default:
178 | return data;
179 | }
180 | });
181 | },
182 | { ...subscribeOptions, expand, filter }
183 | )
184 | .then((value) => {
185 | unsubscribe = value;
186 | });
187 | };
188 |
189 | update((data) => ({ ...data, isLoading: true }));
190 | _pb
191 | ?.collection(collection)
192 | .getList(undefined, undefined, { ...listOptions, sort, expand, filter })
193 | .then((value) => {
194 | update((data) => ({
195 | ...data,
196 | records: value.items as T[],
197 | totalPages: value.totalPages,
198 | totalItems: value.totalItems
199 | }));
200 | subscribe();
201 | })
202 | .catch((reason: ClientResponseError) => {
203 | if (reason.response.status === 404) {
204 | update((data) => ({ ...data, records: null }));
205 | } else {
206 | update((data) => ({ ...data, error: reason }));
207 | }
208 | })
209 | .finally(() => {
210 | update((data) => ({ ...data, isLoading: false }));
211 | });
212 |
213 | return () => {
214 | unsubscribe();
215 | unsubscribe = () => { };
216 | };
217 | }
218 | );
219 | };
220 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | Welcome to the PocketSvelte library
21 |
22 | {#if $auth?.isAuthenticated === false}
23 |
35 | {:else}
36 |
37 | {#snippet loading()}
38 | Loading...
39 | {/snippet}
40 | {#snippet notFound()}
41 | Not found
42 | {/snippet}
43 | {#snippet render(records: PostRecord[])}
44 |
45 | {#each records as record}
46 |
47 | {record.title}
48 |
49 | {/each}
50 |
51 | {/snippet}
52 | {#snippet error(msg)}
53 | {JSON.stringify(msg)}
54 | {/snippet}
55 |
56 |
57 |
58 | {#snippet loading()}
59 | Loading...
60 | {/snippet}
61 | {#snippet render(record: PostRecord)}
62 | {record.title}
63 | {/snippet}
64 | {#snippet notFound()}
65 | Not found
66 | {/snippet}
67 |
68 |
69 |
70 | {#snippet loading()}
71 | Loading...
72 | {/snippet}
73 | {#snippet render(record: PostRecord)}
74 | {record.title}
75 | {/snippet}
76 | {#snippet notFound()}
77 | Not found
78 | {/snippet}
79 |
80 | {/if}
81 |
--------------------------------------------------------------------------------
/src/routes/+page.ts:
--------------------------------------------------------------------------------
1 | export const ssr = false
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brennerm/sveltepocket/156a7938dfba30454f34f922769a1ece876082b4/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://svelte.dev/docs/kit/integrations
7 | // for more information about preprocessors
8 | preprocess: vitePreprocess(),
9 |
10 | kit: {
11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters.
14 | adapter: adapter()
15 | }
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/tests/pb_schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "pbc_526341563",
4 | "listRule": null,
5 | "viewRule": null,
6 | "createRule": null,
7 | "updateRule": null,
8 | "deleteRule": null,
9 | "name": "authors",
10 | "type": "base",
11 | "fields": [
12 | {
13 | "autogeneratePattern": "[a-z0-9]{15}",
14 | "hidden": false,
15 | "id": "text3208210256",
16 | "max": 15,
17 | "min": 15,
18 | "name": "id",
19 | "pattern": "^[a-z0-9]+$",
20 | "presentable": false,
21 | "primaryKey": true,
22 | "required": true,
23 | "system": true,
24 | "type": "text"
25 | },
26 | {
27 | "autogeneratePattern": "",
28 | "hidden": false,
29 | "id": "text1579384326",
30 | "max": 0,
31 | "min": 0,
32 | "name": "name",
33 | "pattern": "",
34 | "presentable": false,
35 | "primaryKey": false,
36 | "required": true,
37 | "system": false,
38 | "type": "text"
39 | },
40 | {
41 | "hidden": false,
42 | "id": "autodate2990389176",
43 | "name": "created",
44 | "onCreate": true,
45 | "onUpdate": false,
46 | "presentable": false,
47 | "system": false,
48 | "type": "autodate"
49 | },
50 | {
51 | "hidden": false,
52 | "id": "autodate3332085495",
53 | "name": "updated",
54 | "onCreate": true,
55 | "onUpdate": true,
56 | "presentable": false,
57 | "system": false,
58 | "type": "autodate"
59 | }
60 | ],
61 | "indexes": [],
62 | "system": false
63 | },
64 | {
65 | "id": "pbc_1125843985",
66 | "listRule": null,
67 | "viewRule": null,
68 | "createRule": null,
69 | "updateRule": null,
70 | "deleteRule": null,
71 | "name": "posts",
72 | "type": "base",
73 | "fields": [
74 | {
75 | "autogeneratePattern": "[a-z0-9]{15}",
76 | "hidden": false,
77 | "id": "text3208210256",
78 | "max": 15,
79 | "min": 15,
80 | "name": "id",
81 | "pattern": "^[a-z0-9]+$",
82 | "presentable": false,
83 | "primaryKey": true,
84 | "required": true,
85 | "system": true,
86 | "type": "text"
87 | },
88 | {
89 | "autogeneratePattern": "",
90 | "hidden": false,
91 | "id": "text724990059",
92 | "max": 0,
93 | "min": 0,
94 | "name": "title",
95 | "pattern": "",
96 | "presentable": false,
97 | "primaryKey": false,
98 | "required": true,
99 | "system": false,
100 | "type": "text"
101 | },
102 | {
103 | "hidden": false,
104 | "id": "bool1748787223",
105 | "name": "published",
106 | "presentable": false,
107 | "required": false,
108 | "system": false,
109 | "type": "bool"
110 | },
111 | {
112 | "cascadeDelete": false,
113 | "collectionId": "pbc_526341563",
114 | "hidden": false,
115 | "id": "relation3182418120",
116 | "maxSelect": 1,
117 | "minSelect": 0,
118 | "name": "author",
119 | "presentable": false,
120 | "required": true,
121 | "system": false,
122 | "type": "relation"
123 | },
124 | {
125 | "hidden": false,
126 | "id": "autodate2990389176",
127 | "name": "created",
128 | "onCreate": true,
129 | "onUpdate": false,
130 | "presentable": false,
131 | "system": false,
132 | "type": "autodate"
133 | },
134 | {
135 | "hidden": false,
136 | "id": "autodate3332085495",
137 | "name": "updated",
138 | "onCreate": true,
139 | "onUpdate": true,
140 | "presentable": false,
141 | "system": false,
142 | "type": "autodate"
143 | }
144 | ],
145 | "indexes": [],
146 | "system": false
147 | }
148 | ]
--------------------------------------------------------------------------------
/tests/vitest-pocketbase-setup.d.ts:
--------------------------------------------------------------------------------
1 | import Pocketbase from 'pocketbase';
2 | export declare const AUTHORS: {
3 | id: string;
4 | name: string;
5 | }[];
6 | export declare const POSTS: {
7 | id: string;
8 | title: string;
9 | author: string;
10 | published: boolean;
11 | }[];
12 | export declare const USERS: {
13 | id: any;
14 | email: string;
15 | password: string;
16 | passwordConfirm: string;
17 | }[];
18 | export declare const pbtest: import("vitest").TestAPI<{
19 | pb: Pocketbase;
20 | }>;
21 |
--------------------------------------------------------------------------------
/tests/vitest-pocketbase-setup.ts:
--------------------------------------------------------------------------------
1 | import { afterAll, beforeAll, beforeEach, test } from 'vitest';
2 | import Pocketbase from 'pocketbase';
3 | import schema from './pb_schema.json'
4 | import { init } from '$lib/stores.svelte.js';
5 | import { randomBytes } from 'crypto';
6 |
7 | export const AUTHORS = [
8 | { id: '000000000000000', name: 'John Doe' },
9 | { id: '000000000000001', name: 'Jane Doe' },
10 | { id: '000000000000002', name: 'Alice' },
11 | { id: '000000000000003', name: 'Bob' },
12 | ]
13 |
14 | export const POSTS = [
15 | { id: '000000000000000', title: 'Post 1', author: '000000000000000', published: true },
16 | { id: '000000000000001', title: 'Post 2', author: '000000000000000', published: false },
17 | { id: '000000000000002', title: 'Post 3', author: '000000000000000', published: true },
18 | { id: '000000000000003', title: 'Post 4', author: '000000000000001', published: false },
19 | { id: '000000000000004', title: 'Post 5', author: '000000000000001', published: true },
20 | { id: '000000000000005', title: 'Post 6', author: '000000000000002', published: false },
21 | { id: '000000000000006', title: 'Post 7', author: '000000000000003', published: true },
22 | { id: '000000000000007', title: 'Post 8', author: '000000000000003', published: false },
23 | { id: '000000000000008', title: 'Post 9', author: '000000000000003', published: true },
24 | { id: '000000000000009', title: 'Post 10', author: '000000000000003', published: false },
25 | ]
26 |
27 | export const USERS = [
28 | { id: randomBytes(20).toString('hex').substring(0, 15), email: `${randomBytes(5).toString('hex')}@bar.com`, password: 'password', passwordConfirm: 'password' },
29 | ]
30 |
31 | interface PbTestFixtures {
32 | pb: Pocketbase
33 | }
34 |
35 | const pb = new Pocketbase('http://localhost:8090');
36 |
37 | export const pbtest = test.extend({
38 | pb: pb
39 | })
40 |
41 | beforeAll(async ({ id }) => {
42 | await pb.collection('_superusers').authWithPassword('pocketbase@svelte.com', 'pocketbase');
43 | await pb.collections.import(schema)
44 |
45 | for (const user of USERS) {
46 | await pb.collection('users').create(user)
47 | }
48 |
49 | init(pb);
50 | });
51 |
52 | beforeEach(async () => {
53 | await pb.collection('_superusers').authWithPassword('pocketbase@svelte.com', 'pocketbase');
54 | await pb.collections.truncate('posts')
55 | await pb.collections.truncate('authors')
56 |
57 | for (const author of AUTHORS) {
58 | await pb.collection('authors').create(author)
59 | }
60 |
61 | for (const post of POSTS) {
62 | await pb.collection('posts').create(post)
63 | }
64 | });
65 |
66 | afterAll(async () => {
67 | await pb.collection('_superusers').authWithPassword('pocketbase@svelte.com', 'pocketbase');
68 | await pb.collection('users').delete(USERS[0].id)
69 | });
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "module": "NodeNext",
13 | "moduleResolution": "NodeNext"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { svelteTesting } from '@testing-library/svelte/vite';
2 | import { sveltekit } from '@sveltejs/kit/vite';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [sveltekit()],
7 |
8 | test: {
9 | workspace: [
10 | {
11 | extends: './vite.config.ts',
12 | plugins: [svelteTesting()],
13 |
14 | test: {
15 | name: 'client',
16 | environment: 'jsdom',
17 | clearMocks: true,
18 | include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
19 | exclude: ['src/lib/server/**'],
20 | setupFiles: ['./vitest-setup-client.ts'],
21 | // poolOptions: {
22 | // threads: {
23 | // singleThread: true
24 | // }
25 | // }
26 | }
27 | },
28 | // {
29 | // extends: './vite.config.ts',
30 |
31 | // test: {
32 | // name: 'server',
33 | // environment: 'node',
34 | // include: ['src/**/*.{test,spec}.{js,ts}'],
35 | // exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
36 | // }
37 | // }
38 | ],
39 | setupFiles: ['./tests/vitest-pocketbase-setup.ts'],
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/vitest-setup-client.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest';
2 | import { vi } from 'vitest';
3 |
4 | // required for svelte5 + jsdom as jsdom does not support matchMedia
5 | Object.defineProperty(window, 'matchMedia', {
6 | writable: true,
7 | enumerable: true,
8 | value: vi.fn().mockImplementation((query) => ({
9 | matches: false,
10 | media: query,
11 | onchange: null,
12 | addEventListener: vi.fn(),
13 | removeEventListener: vi.fn(),
14 | dispatchEvent: vi.fn()
15 | }))
16 | });
17 |
18 | // add more mocks here if you need them
19 |
--------------------------------------------------------------------------------