diff --git a/README.md b/README.md index 04eeac8..d99bba3 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ console.log(allPlaces); // List places for specific geohash prefixes (e.g. for a map view) const areaPlaces = await places.getPlaces(['u33d', 'u33e']); console.log(areaPlaces); + +// Create a list +await places.lists.create('favorites', 'My Favorites'); + +// Add a place to a list (requires list ID, place ID, and place geohash) +await places.lists.addPlace('favorites', 'place-id-123', 'u33dc0'); ``` ## API Reference @@ -52,3 +58,4 @@ console.log(areaPlaces); ### Type Aliases - [Place](docs/type-aliases/Place.md) +- [List](docs/type-aliases/List.md) diff --git a/docs/README.md b/docs/README.md index abc164d..5768872 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ ## Type Aliases +- [List](type-aliases/List.md) - [Place](type-aliases/Place.md) ## Variables diff --git a/docs/interfaces/PlacesClient.md b/docs/interfaces/PlacesClient.md index b247cbb..f455c71 100644 --- a/docs/interfaces/PlacesClient.md +++ b/docs/interfaces/PlacesClient.md @@ -6,6 +6,144 @@ # Interface: PlacesClient +## Properties + +### lists + +> **lists**: `object` + +#### addPlace() + +> **addPlace**(`listId`, `placeId`, `geohash`): `Promise`\<[`List`](../type-aliases/List.md)\> + +Add a place to a list. + +##### Parameters + +###### listId + +`string` + +The slug ID of the list. + +###### placeId + +`string` + +The ID of the place. + +###### geohash + +`string` + +The geohash of the place. + +##### Returns + +`Promise`\<[`List`](../type-aliases/List.md)\> + +#### create() + +> **create**(`id`, `title`, `color?`): `Promise`\<[`List`](../type-aliases/List.md)\> + +Create or update a list. + +##### Parameters + +###### id + +`string` + +The slug ID (e.g., "to-go"). + +###### title + +`string` + +Human readable title. + +###### color? + +`string` + +Optional hex color code. + +##### Returns + +`Promise`\<[`List`](../type-aliases/List.md)\> + +#### delete() + +> **delete**(`id`): `Promise`\<`void`\> + +Delete a list. + +##### Parameters + +###### id + +`string` + +The slug ID of the list. + +##### Returns + +`Promise`\<`void`\> + +#### get() + +> **get**(`id`): `Promise`\<[`List`](../type-aliases/List.md) \| `null`\> + +Get a single list by ID (slug). + +##### Parameters + +###### id + +`string` + +The slug ID of the list. + +##### Returns + +`Promise`\<[`List`](../type-aliases/List.md) \| `null`\> + +#### getAll() + +> **getAll**(): `Promise`\<[`List`](../type-aliases/List.md)[]\> + +Get all lists. + +##### Returns + +`Promise`\<[`List`](../type-aliases/List.md)[]\> + +Array of List objects. + +#### removePlace() + +> **removePlace**(`listId`, `placeId`): `Promise`\<[`List`](../type-aliases/List.md)\> + +Remove a place from a list. + +##### Parameters + +###### listId + +`string` + +The slug ID of the list. + +###### placeId + +`string` + +The ID of the place. + +##### Returns + +`Promise`\<[`List`](../type-aliases/List.md)\> + ## Methods ### get() diff --git a/docs/type-aliases/List.md b/docs/type-aliases/List.md new file mode 100644 index 0000000..3e9cfaf --- /dev/null +++ b/docs/type-aliases/List.md @@ -0,0 +1,9 @@ +[**@remotestorage/module-places**](../README.md) + +*** + +[@remotestorage/module-places](../README.md) / List + +# Type Alias: List + +> **List** = `FromSchema`\<*typeof* `listSchema`\> & `object` diff --git a/package.json b/package.json index 405e138..62f80cc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "scripts": { "build": "rimraf dist && tsc", "doc": "typedoc", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "vitest run", + "test:watch": "vitest", + "preversion": "pnpm test", "version": "pnpm run build && pnpm run doc && git add dist docs README.md" }, "author": "Râu Cao ", @@ -31,7 +33,8 @@ "rimraf": "^6.1.2", "typedoc": "^0.28.16", "typedoc-plugin-markdown": "^4.9.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.0.18" }, "dependencies": { "latlon-geohash": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b79738..ff12c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: typescript: specifier: ^5.9.3 version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(yaml@2.8.2) packages: @@ -40,6 +43,162 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@gerrit0/mini-shiki@3.21.0': resolution: {integrity: sha512-9PrsT5DjZA+w3lur/aOIx3FlDeHdyCEFlv9U+fmsVyjPZh61G5SYURQ/1ebe2U63KbDmI2V8IhIUegWb8hjOyg==} @@ -51,6 +210,147 @@ packages: resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} engines: {node: 20 || >=22} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.59.0': + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.59.0': + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.59.0': + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.59.0': + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.59.0': + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.59.0': + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.59.0': + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.59.0': + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.59.0': + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.59.0': + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.59.0': + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.59.0': + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.59.0': + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.59.0': + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + cpu: [x64] + os: [win32] + '@shikijs/engine-oniguruma@3.21.0': resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} @@ -66,6 +366,18 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -78,23 +390,84 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -121,6 +494,9 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -140,6 +516,14 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -147,6 +531,20 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -159,6 +557,39 @@ packages: engines: {node: 20 || >=22} hasBin: true + rollup@4.59.0: + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -194,9 +625,88 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + webfinger.js@2.8.2: resolution: {integrity: sha512-Zqn9KXkGrD1tVEm029bVUIfmzef2KXs3G7OZrdqehDHtgv9YSxX1oy4RoPoMk2PHWIifwWCA0xwKZOAZqXMpfg==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + xhr2@0.2.1: resolution: {integrity: sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==} engines: {node: '>= 6'} @@ -210,6 +720,84 @@ snapshots: '@babel/runtime@7.28.6': {} + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + '@gerrit0/mini-shiki@3.21.0': dependencies: '@shikijs/engine-oniguruma': 3.21.0 @@ -224,6 +812,83 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.59.0': + optional: true + + '@rollup/rollup-android-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.59.0': + optional: true + + '@rollup/rollup-darwin-x64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.59.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.59.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.59.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.59.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.59.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.59.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.59.0': + optional: true + '@shikijs/engine-oniguruma@3.21.0': dependencies: '@shikijs/types': 3.21.0 @@ -244,6 +909,17 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -256,18 +932,102 @@ snapshots: '@types/unist@3.0.3': {} + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.0.18(vite@7.3.1(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.1.0 + argparse@2.0.1: {} + assertion-error@2.0.1: {} + balanced-match@1.0.2: {} brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 + chai@6.2.2: {} + entities@4.5.0: {} + es-module-lexer@1.7.0: {} + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + esm@3.2.25: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + fsevents@2.3.3: optional: true @@ -292,6 +1052,10 @@ snapshots: lunr@2.3.9: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -313,6 +1077,10 @@ snapshots: minipass@7.1.2: {} + nanoid@3.3.11: {} + + obug@2.1.1: {} + package-json-from-dist@1.0.1: {} path-scurry@2.0.1: @@ -320,6 +1088,18 @@ snapshots: lru-cache: 11.2.4 minipass: 7.1.2 + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + punycode.js@2.3.1: {} remotestoragejs@2.0.0-beta.8: @@ -338,6 +1118,56 @@ snapshots: glob: 13.0.0 package-json-from-dist: 1.0.1 + rollup@4.59.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.59.0 + '@rollup/rollup-android-arm64': 4.59.0 + '@rollup/rollup-darwin-arm64': 4.59.0 + '@rollup/rollup-darwin-x64': 4.59.0 + '@rollup/rollup-freebsd-arm64': 4.59.0 + '@rollup/rollup-freebsd-x64': 4.59.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 + '@rollup/rollup-linux-arm64-musl': 4.59.0 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 + '@rollup/rollup-linux-loong64-musl': 4.59.0 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 + '@rollup/rollup-linux-x64-gnu': 4.59.0 + '@rollup/rollup-linux-x64-musl': 4.59.0 + '@rollup/rollup-openbsd-x64': 4.59.0 + '@rollup/rollup-openharmony-arm64': 4.59.0 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 + '@rollup/rollup-win32-x64-gnu': 4.59.0 + '@rollup/rollup-win32-x64-msvc': 4.59.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyrainbow@3.1.0: {} + ts-algebra@2.0.0: {} tv4@1.3.0: {} @@ -363,8 +1193,60 @@ snapshots: undici-types@5.26.5: {} + vite@7.3.1(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + yaml: 2.8.2 + + vitest@4.0.18(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 7.3.1(yaml@2.8.2) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + webfinger.js@2.8.2: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + xhr2@0.2.1: {} yaml@2.8.2: {} diff --git a/src/places.ts b/src/places.ts index cb691e4..f4704b5 100644 --- a/src/places.ts +++ b/src/places.ts @@ -31,6 +31,32 @@ const placeSchema = { required: ['id', 'title', 'lat', 'lon', 'geohash', 'createdAt'], } as const; +const listSchema = { + type: 'object', + properties: { + id: { type: 'string' }, + title: { type: 'string' }, + color: { type: 'string' }, + placeRefs: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + geohash: { type: 'string' }, + }, + required: ['id', 'geohash'], + }, + default: [], + }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, + }, + required: ['id', 'title', 'placeRefs', 'createdAt'], +} as const; + +export type List = FromSchema & { [key: string]: any }; + /** * Represents a Place object. * @@ -101,6 +127,49 @@ export interface PlacesClient { * @returns An array of places. */ getPlaces(prefixes?: string[]): Promise; + + lists: { + /** + * Get all lists. + * @returns Array of List objects. + */ + getAll(): Promise; + + /** + * Get a single list by ID (slug). + * @param id - The slug ID of the list. + */ + get(id: string): Promise; + + /** + * Create or update a list. + * @param id - The slug ID (e.g., "to-go"). + * @param title - Human readable title. + * @param color - Optional hex color code. + */ + create(id: string, title: string, color?: string): Promise; + + /** + * Delete a list. + * @param id - The slug ID of the list. + */ + delete(id: string): Promise; + + /** + * Add a place to a list. + * @param listId - The slug ID of the list. + * @param placeId - The ID of the place. + * @param geohash - The geohash of the place. + */ + addPlace(listId: string, placeId: string, geohash: string): Promise; + + /** + * Remove a place from a list. + * @param listId - The slug ID of the list. + * @param placeId - The ID of the place. + */ + removePlace(listId: string, placeId: string): Promise; + }; } const Places = function ( @@ -108,6 +177,7 @@ const Places = function ( ): { exports: PlacesClient } { // Define Schema privateClient.declareType('place', placeSchema as any); + privateClient.declareType('list', listSchema as any); // Helper to normalize place object function preparePlace(data: Partial): Place { @@ -153,7 +223,110 @@ const Places = function ( return `${p1}/${p2}/${id}`; } + const lists = { + async getAll(): Promise { + const result = await privateClient.getAll('_lists/'); + if (!result) return []; + // Normalize result: remoteStorage.getAll returns { 'slug': object } + return Object.values(result); + }, + + async get(id: string): Promise { + const path = `_lists/${id}`; + return privateClient.getObject(path) as Promise; + }, + + async create(id: string, title: string, color?: string): Promise { + const path = `_lists/${id}`; + let list = (await privateClient.getObject(path)) as List; + const now = new Date().toISOString(); + + if (list) { + // Update existing + list.title = title; + if (color) list.color = color; + list.updatedAt = now; + } else { + // Create new + list = { + id, + title, + color, + placeRefs: [], + createdAt: now, + updatedAt: now, + } as List; + } + + await privateClient.storeObject('list', path, list); + return list; + }, + + async delete(id: string): Promise { + await privateClient.remove(`_lists/${id}`); + }, + + async addPlace( + listId: string, + placeId: string, + geohash: string + ): Promise { + const path = `_lists/${listId}`; + const list = (await privateClient.getObject(path)) as List; + + if (!list) { + throw new Error(`List not found: ${listId}`); + } + + const index = list.placeRefs.findIndex((ref: any) => ref.id === placeId); + + if (index === -1) { + // Add only if not present + list.placeRefs.push({ id: placeId, geohash }); + list.updatedAt = new Date().toISOString(); + await privateClient.storeObject('list', path, list); + } + + return list; + }, + + async removePlace(listId: string, placeId: string): Promise { + const path = `_lists/${listId}`; + const list = (await privateClient.getObject(path)) as List; + + if (!list) { + throw new Error(`List not found: ${listId}`); + } + + const index = list.placeRefs.findIndex((ref: any) => ref.id === placeId); + + if (index !== -1) { + // Remove only if present + list.placeRefs.splice(index, 1); + list.updatedAt = new Date().toISOString(); + await privateClient.storeObject('list', path, list); + } + + return list; + }, + + async initDefaults(): Promise { + const defaults = [ + { id: 'to-go', title: 'Want to go', color: '#2e9e4f' }, // Green + { id: 'to-do', title: 'To do', color: '#2a7fff' }, // Blue + ]; + + for (const def of defaults) { + const existing = await this.get(def.id); + if (!existing) { + await this.create(def.id, def.title, def.color); + } + } + }, + }; + const places = { + lists, /** * Store a place. * Generates ID and Geohash if missing. @@ -174,6 +347,20 @@ const Places = function ( if (!id || !geohash) { throw new Error('Both id and geohash are required to remove a place'); } + + // Cleanup: Remove this place from all lists + const allLists = await lists.getAll(); + await Promise.all( + allLists.map(async (list) => { + const index = list.placeRefs.findIndex((ref: any) => ref.id === id); + if (index !== -1) { + list.placeRefs.splice(index, 1); + list.updatedAt = new Date().toISOString(); + await privateClient.storeObject('list', `_lists/${list.id}`, list); + } + }) + ); + const path = getPath(geohash, id); return privateClient.remove(path); }, diff --git a/test/places.test.ts b/test/places.test.ts new file mode 100644 index 0000000..7acaee0 --- /dev/null +++ b/test/places.test.ts @@ -0,0 +1,414 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import PlacesModule from '../src/places.js'; +import { createMockClient, MockPrivateClient } from './utils.js'; + +// We need to cast our mock to any because we are not implementing the full BaseClient interface +const Places = PlacesModule.builder; + +describe('Places Module', () => { + let mockClient: MockPrivateClient; + let moduleInstance: any; + + beforeEach(() => { + mockClient = createMockClient(); + moduleInstance = Places(mockClient as any); + }); + + describe('Places Functionality', () => { + let places: any; + + beforeEach(() => { + places = moduleInstance.exports; + }); + + describe('store', () => { + it('saves a place with the correct path structure', async () => { + const placeData = { + title: 'Test Place', + lat: 52.5, + lon: 13.4, + }; + + await places.store(placeData); + + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'place', + expect.stringMatching(/^u3\/3d\/.+/), + expect.objectContaining({ + title: 'Test Place', + lat: 52.5, + lon: 13.4, + }) + ); + }); + }); + + describe('remove', () => { + it('deletes a place at the correct path', async () => { + const id = 'some-id'; + const geohash = 'u33dc0'; // u3/3d/ + + mockClient.getAll.mockResolvedValue([]); + + await places.remove(id, geohash); + + expect(mockClient.remove).toHaveBeenCalledWith('u3/3d/some-id'); + }); + + it('cleans up references from lists', async () => { + const id = 'some-id'; + const geohash = 'u33dc0'; + + const mockLists = { + 'to-go': { + id: 'to-go', + placeRefs: [{ id: 'some-id', geohash: 'u33dc0' }], + }, + 'to-do': { id: 'to-do', placeRefs: [] }, + }; + mockClient.getAll.mockResolvedValue(Object.values(mockLists)); + + await places.remove(id, geohash); + + expect(mockClient.getAll).toHaveBeenCalledWith('_lists/'); + + // Expect "to-go" to be updated without the reference + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/to-go', + expect.objectContaining({ + placeRefs: [], + }) + ); + }); + }); + + describe('get', () => { + it('retrieves a single place at the correct path', async () => { + const id = 'some-id'; + const geohash = 'u33dc0'; + const mockPlace = { id, geohash, title: 'Test Place' }; + mockClient.getObject.mockResolvedValue(mockPlace); + + const result = await places.get(id, geohash); + + expect(mockClient.getObject).toHaveBeenCalledWith('u3/3d/some-id'); + expect(result).toEqual(mockPlace); + }); + }); + + describe('listByPrefix', () => { + it('lists subfolders for 2-char prefix', async () => { + const prefix = 'u3'; + const mockListing = { '3d/': true }; + mockClient.getAll.mockResolvedValue(mockListing); + + const result = await places.listByPrefix(prefix); + + expect(mockClient.getAll).toHaveBeenCalledWith('u3/'); + expect(result).toEqual(mockListing); + }); + + it('lists places for 4-char prefix', async () => { + const prefix = 'u33d'; + const mockPlaces = { place1: { title: 'Place 1' } }; + mockClient.getAll.mockResolvedValue(mockPlaces); + + const result = await places.listByPrefix(prefix); + + expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/'); + expect(result).toEqual(mockPlaces); + }); + }); + + describe('getPlaces', () => { + it('fetches places from specified prefixes', async () => { + const prefixes = ['u33d', 'w1q7']; + const mockPlaces1 = { p1: { id: 'p1', geohash: 'u33d' } }; + const mockPlaces2 = { p2: { id: 'p2', geohash: 'w1q7' } }; + + mockClient.getAll.mockImplementation(async (path: string) => { + if (path === 'u3/3d/') return mockPlaces1; + if (path === 'w1/q7/') return mockPlaces2; + return {}; + }); + + const result = await places.getPlaces(prefixes); + + expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/', false); + expect(mockClient.getAll).toHaveBeenCalledWith('w1/q7/', false); + expect(result).toHaveLength(2); + expect(result).toContainEqual(mockPlaces1['p1']); + expect(result).toContainEqual(mockPlaces2['p2']); + }); + + it('recursively fetches all places when no prefix is provided', async () => { + // Mock directory structure + // root -> 'u3/' + // 'u3/' -> '3d/' + // 'u3/3d/' -> place1 + + mockClient.getListing.mockImplementation(async (path: string) => { + if (path === '') return { 'u3/': true }; + if (path === 'u3/') return { '3d/': true }; + return {}; + }); + + const mockPlaces = { place1: { id: 'p1', geohash: 'u33d' } }; + mockClient.getAll.mockImplementation(async (path: string) => { + if (path === 'u3/3d/') return mockPlaces; + return {}; + }); + + const result = await places.getPlaces(); + + expect(mockClient.getListing).toHaveBeenCalledWith('', false); + expect(mockClient.getListing).toHaveBeenCalledWith('u3/', false); + expect(mockClient.getAll).toHaveBeenCalledWith('u3/3d/', false); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(mockPlaces['place1']); + }); + }); + }); + + describe('Lists Functionality', () => { + let lists: any; + + beforeEach(() => { + lists = moduleInstance.exports.lists; + }); + + describe('getAll', () => { + it('returns all lists from the _lists/ directory', async () => { + const mockLists = { + 'to-go': { id: 'to-go', title: 'Want to go' }, + hiking: { id: 'hiking', title: 'Hiking' }, + }; + mockClient.getAll.mockResolvedValue(mockLists); + + const result = await lists.getAll(); + expect(mockClient.getAll).toHaveBeenCalledWith('_lists/'); + expect(result).toEqual(Object.values(mockLists)); + }); + }); + + describe('get', () => { + it('returns a single list by ID', async () => { + const mockList = { id: 'hiking', title: 'Hiking' }; + mockClient.getObject.mockResolvedValue(mockList); + + const result = await lists.get('hiking'); + expect(mockClient.getObject).toHaveBeenCalledWith('_lists/hiking'); + expect(result).toEqual(mockList); + }); + }); + + describe('create', () => { + it('stores a new list when none exists', async () => { + const now = '2023-01-01T00:00:00.000Z'; + vi.setSystemTime(new Date(now)); + + // Mock getObject to return null (not existing) + mockClient.getObject.mockResolvedValue(null); + + const result = await lists.create('hiking', 'Hiking', '#00ff00'); + + expect(mockClient.getObject).toHaveBeenCalledWith('_lists/hiking'); + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/hiking', + { + id: 'hiking', + title: 'Hiking', + color: '#00ff00', + placeRefs: [], + createdAt: now, + updatedAt: now, + } + ); + expect(result).toMatchObject({ id: 'hiking', title: 'Hiking' }); + + vi.useRealTimers(); + }); + + it('updates an existing list preserving creation time and references', async () => { + const now = '2023-01-02T00:00:00.000Z'; + vi.setSystemTime(new Date(now)); + + const existing = { + id: 'hiking', + title: 'Old Title', + color: '#ffffff', + placeRefs: [{ id: '123', geohash: 'abc' }], + createdAt: '2022-01-01T00:00:00.000Z', + }; + mockClient.getObject.mockResolvedValue(existing); + + await lists.create('hiking', 'Hiking Updated', '#000000'); + + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/hiking', + expect.objectContaining({ + id: 'hiking', + title: 'Hiking Updated', + color: '#000000', + placeRefs: existing.placeRefs, // Should preserve refs + createdAt: existing.createdAt, // Should preserve createdAt + updatedAt: now, // Should update updatedAt + }) + ); + + vi.useRealTimers(); + }); + }); + + describe('delete', () => { + it('removes the list document', async () => { + await lists.delete('hiking'); + expect(mockClient.remove).toHaveBeenCalledWith('_lists/hiking'); + }); + }); + + describe('addPlace', () => { + it('adds a place reference when not present', async () => { + const now = '2023-01-03T00:00:00.000Z'; + vi.setSystemTime(new Date(now)); + + const list = { + id: 'hiking', + placeRefs: [], + updatedAt: 'old-date', + }; + mockClient.getObject.mockResolvedValue(list); + + await lists.addPlace('hiking', 'place-123', 'w1q7'); + + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/hiking', + expect.objectContaining({ + placeRefs: [{ id: 'place-123', geohash: 'w1q7' }], + updatedAt: now, + }) + ); + + vi.useRealTimers(); + }); + + it('does nothing if place is already present', async () => { + const list = { + id: 'hiking', + placeRefs: [{ id: 'place-123', geohash: 'w1q7' }], + updatedAt: 'old-date', + }; + mockClient.getObject.mockResolvedValue(list); + + await lists.addPlace('hiking', 'place-123', 'w1q7'); + + expect(mockClient.storeObject).not.toHaveBeenCalled(); + }); + }); + + describe('removePlace', () => { + it('removes a place reference when present', async () => { + const now = '2023-01-04T00:00:00.000Z'; + vi.setSystemTime(new Date(now)); + + const list = { + id: 'hiking', + placeRefs: [{ id: 'place-123', geohash: 'w1q7' }], + updatedAt: 'old-date', + }; + mockClient.getObject.mockResolvedValue(list); + + await lists.removePlace('hiking', 'place-123'); + + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/hiking', + expect.objectContaining({ + placeRefs: [], + updatedAt: now, + }) + ); + + vi.useRealTimers(); + }); + + it('does nothing if place is not present', async () => { + const list = { + id: 'hiking', + placeRefs: [], + updatedAt: 'old-date', + }; + mockClient.getObject.mockResolvedValue(list); + + await lists.removePlace('hiking', 'place-123'); + + expect(mockClient.storeObject).not.toHaveBeenCalled(); + }); + }); + + describe('initDefaults', () => { + it('creates "Want to go" list if missing', async () => { + mockClient.getObject.mockResolvedValue(null); + + await lists.initDefaults(); + + expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-go'); + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/to-go', + expect.objectContaining({ + title: 'Want to go', + id: 'to-go', + color: '#2e9e4f', + }) + ); + }); + + it('creates "To do" list if missing', async () => { + mockClient.getObject.mockResolvedValue(null); + + await lists.initDefaults(); + + expect(mockClient.getObject).toHaveBeenCalledWith('_lists/to-do'); + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/to-do', + expect.objectContaining({ + title: 'To do', + id: 'to-do', + color: '#2a7fff', + }) + ); + }); + + it('does not overwrite existing lists', async () => { + // Mock that "to-go" exists but "to-do" does not + mockClient.getObject.mockImplementation(async (path: string) => { + if (path === '_lists/to-go') + return { id: 'to-go', title: 'Existing' }; + return null; + }); + + await lists.initDefaults(); + + // Should NOT write to-go + expect(mockClient.storeObject).not.toHaveBeenCalledWith( + 'list', + '_lists/to-go', + expect.anything() + ); + + // Should write to-do + expect(mockClient.storeObject).toHaveBeenCalledWith( + 'list', + '_lists/to-do', + expect.anything() + ); + }); + }); + }); +}); diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..e728d31 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,16 @@ +import { vi } from 'vitest'; + +export class MockPrivateClient { + storeObject = vi.fn(); + remove = vi.fn(); + getObject = vi.fn(); + getAll = vi.fn(); + getListing = vi.fn(); + declareType = vi.fn(); + + // Helper to verify calls easily if needed directly +} + +export function createMockClient() { + return new MockPrivateClient(); +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..1117b8f --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', // Using node environment as we are testing logic not browser specifics + include: ['test/**/*.test.ts'], + deps: { + interopDefault: true, + }, + }, +});