From c6b2298fe6098b11f09b850d916da5c1488bb21f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 19:45:59 +0000 Subject: [PATCH 01/14] fix(geocoding): replace deprecated AutocompleteService with PlaceAutocompleteElement Google deprecated google.maps.places.AutocompleteService as of March 2025. This replaces the react-google-places-autocomplete dependency with Google's new PlaceAutocompleteElement web component and fetchFields API. https://claude.ai/code/session_01TZQpaQ1MNq3SQ8uEU7iCSt --- geocoding/package.json | 16 +- geocoding/pnpm-lock.yaml | 65 +------ .../src/components/GeocodingFieldClient.tsx | 174 ++++++++++++++---- 3 files changed, 150 insertions(+), 105 deletions(-) diff --git a/geocoding/package.json b/geocoding/package.json index dcb891fa..b1142b80 100644 --- a/geocoding/package.json +++ b/geocoding/package.json @@ -6,16 +6,8 @@ "license": "MIT", "bugs": "https://github.com/jhb-software/payload-plugins/issues", "repository": "https://github.com/jhb-software/payload-plugins", - "keywords": [ - "payload", - "plugin", - "geocoding" - ], - "files": [ - "dist", - "LICENSE.md", - "README.md" - ], + "keywords": ["payload", "plugin", "geocoding"], + "files": ["dist", "LICENSE.md", "README.md"], "main": "./src/index.ts", "types": "./src/index.ts", "type": "module", @@ -32,9 +24,6 @@ "prepack": "pnpm prepublishOnly", "prepublishOnly": "pnpm build && pnpm copyfiles" }, - "dependencies": { - "react-google-places-autocomplete": "^4.1.0" - }, "peerDependencies": { "@payloadcms/ui": "^3.81.0", "next": "15.4.11", @@ -44,6 +33,7 @@ }, "devDependencies": { "@payloadcms/eslint-config": "^3.28.0", + "@types/google.maps": "^3.58.1", "@swc/cli": "^0.8.1", "@swc/core": "^1.15.24", "@types/react": "19.2.14", diff --git a/geocoding/pnpm-lock.yaml b/geocoding/pnpm-lock.yaml index 6c635de9..957a441b 100644 --- a/geocoding/pnpm-lock.yaml +++ b/geocoding/pnpm-lock.yaml @@ -23,9 +23,6 @@ importers: react-dom: specifier: 19.2.4 version: 19.2.4(react@19.2.4) - react-google-places-autocomplete: - specifier: ^4.1.0 - version: 4.1.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: '@payloadcms/eslint-config': specifier: ^3.28.0 @@ -36,6 +33,9 @@ importers: '@swc/core': specifier: ^1.15.24 version: 1.15.24 + '@types/google.maps': + specifier: ^3.58.1 + version: 3.58.1 '@types/react': specifier: 19.2.14 version: 19.2.14 @@ -622,9 +622,6 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} - '@googlemaps/js-api-loader@1.16.10': - resolution: {integrity: sha512-c2erv2k7P2ilYzMmtYcMgAR21AULosQuUHJbStnrvRk2dG93k5cqptDrh9A8p+ZNlyhiqEOgHW7N9PAizdUM7Q==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -3137,12 +3134,6 @@ packages: peerDependencies: react: ^19.2.4 - react-google-places-autocomplete@4.1.0: - resolution: {integrity: sha512-C+BOJ/2667DLAFpd9To/OZJm1+1MOp7J6fQihys/W89tDcXb0O7i5ea7zOZ58ZE0mydnz788WxWKpqBqQJHjJg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-image-crop@10.1.8: resolution: {integrity: sha512-4rb8XtXNx7ZaOZarKKnckgz4xLMvds/YrU6mpJfGhGAsy2Mg4mIw1x+DCCGngVGq2soTBVVOxx2s/C6mTX9+pA==} peerDependencies: @@ -3151,12 +3142,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-select@5.10.2: - resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-select@5.9.0: resolution: {integrity: sha512-nwRKGanVHGjdccsnzhFte/PULziueZxGD8LL2WojON78Mvnq7LdAMEtu2frrwld1fr3geixg3iiMBIc/LLAZpw==} peerDependencies: @@ -3666,11 +3651,6 @@ packages: react: '>=18.0.0' scheduler: '>=0.19.0' - use-debounce@3.4.3: - resolution: {integrity: sha512-nxy+opOxDccWfhMl36J5BSCTpvcj89iaQk2OZWLAtBJQj7ISCtx1gh+rFbdjGfMl6vtCZf6gke/kYvrkVfHMoA==} - peerDependencies: - react: '>=16.8.0' - use-isomorphic-layout-effect@1.2.1: resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} peerDependencies: @@ -4661,8 +4641,6 @@ snapshots: '@floating-ui/utils@0.2.11': {} - '@googlemaps/js-api-loader@1.16.10': {} - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -5537,7 +5515,7 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 - '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.9.3))(eslint@9.22.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.7.3) @@ -7609,41 +7587,12 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 - react-google-places-autocomplete@4.1.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@googlemaps/js-api-loader': 1.16.10 - '@types/google.maps': 3.58.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-select: 5.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - use-debounce: 3.4.3(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - supports-color - react-image-crop@10.1.8(react@19.2.4): dependencies: react: 19.2.4 react-is@16.13.1: {} - react-select@5.10.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - '@emotion/cache': 11.14.0 - '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.4) - '@floating-ui/dom': 1.7.6 - '@types/react-transition-group': 4.4.12(@types/react@19.2.14) - memoize-one: 6.0.0 - prop-types: 15.8.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) - transitivePeerDependencies: - - '@types/react' - - supports-color - react-select@5.9.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@babel/runtime': 7.29.2 @@ -8192,7 +8141,7 @@ snapshots: typescript-eslint@8.26.1(eslint@9.22.0)(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3) + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.9.3))(eslint@9.22.0)(typescript@5.7.3) '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.7.3) '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.7.3) eslint: 9.22.0 @@ -8235,10 +8184,6 @@ snapshots: react: 19.2.4 scheduler: 0.25.0 - use-debounce@3.4.3(react@19.2.4): - dependencies: - react: 19.2.4 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): dependencies: react: 19.2.4 diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 14b36ba8..3636aee3 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -2,15 +2,7 @@ import type { FieldBaseClient } from 'payload' import { FieldError, FieldLabel, useField } from '@payloadcms/ui' -import GooglePlacesAutocompleteImport, { - geocodeByPlaceId, - getLatLng, -} from 'react-google-places-autocomplete' - -// Workaround for TypeScript moduleResolution: "nodenext" with React 19 -const GooglePlacesAutocomplete = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - GooglePlacesAutocompleteImport as unknown as React.ComponentType +import { useCallback, useEffect, useRef, useState } from 'react' interface GeocodingFieldComponentProps { field: Pick @@ -18,9 +10,42 @@ interface GeocodingFieldComponentProps { path: string } +/** + * Loads the Google Maps JS API script if not already present. + * Returns a promise that resolves when the API is ready. + */ +function loadGoogleMapsApi(apiKey: string): Promise { + return new Promise((resolve, reject) => { + if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { + resolve() + return + } + + const existingScript = document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]') + if (existingScript) { + existingScript.addEventListener('load', () => resolve()) + existingScript.addEventListener('error', () => + reject(new Error('Failed to load Google Maps API')), + ) + return + } + + const script = document.createElement('script') + script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&libraries=places&loading=async` + script.async = true + script.defer = true + script.onload = () => resolve() + script.onerror = () => reject(new Error('Failed to load Google Maps API')) + document.head.appendChild(script) + }) +} + /** * A custom client component that shows the Google Places Autocomplete component and * fills the point and geodata fields with the received data from the Google Places API. + * + * Uses the new PlaceAutocompleteElement (google.maps.places) instead of the deprecated + * AutocompleteService. */ export const GeocodingFieldClient = ({ field, @@ -34,37 +59,122 @@ export const GeocodingFieldClient = ({ }) const { setValue: setPoint } = useField>({ path: pointFieldPath }) + const containerRef = useRef(null) + const autocompleteRef = useRef(null) + const [error, setError] = useState(null) + + const handlePlaceSelect = useCallback( + async (event: Event) => { + const placeEvent = event as google.maps.places.PlaceAutocompletePlaceSelectEvent + const place = placeEvent.place + + if (!place) { + return + } + + await place.fetchFields({ + fields: ['displayName', 'formattedAddress', 'location'], + }) + + if (place.location) { + setPoint([place.location.lng(), place.location.lat()]) + setGeoData({ + label: place.formattedAddress ?? place.displayName, + value: { + description: place.formattedAddress, + place_id: place.id, + structured_formatting: { + main_text: place.displayName, + }, + }, + }) + } + }, + [setGeoData, setPoint], + ) + + useEffect(() => { + let cancelled = false + + const init = async () => { + try { + await loadGoogleMapsApi(googleMapsApiKey) + if (cancelled || !containerRef.current) { + return + } + + await google.maps.importLibrary('places') + if (cancelled || !containerRef.current) { + return + } + + // Don't re-create if already initialized + if (autocompleteRef.current) { + return + } + + const autocomplete = new google.maps.places.PlaceAutocompleteElement({}) + autocompleteRef.current = autocomplete + + // Style the input to fill the container + autocomplete.style.width = '100%' + + autocomplete.addEventListener('gmp-placeselect', handlePlaceSelect) + + containerRef.current.appendChild(autocomplete) + } catch (e) { + if (!cancelled) { + setError(e instanceof Error ? e.message : 'Failed to load Google Maps') + } + } + } + + void init() + + return () => { + cancelled = true + if (autocompleteRef.current) { + autocompleteRef.current.removeEventListener('gmp-placeselect', handlePlaceSelect) + } + } + }, [googleMapsApiKey, handlePlaceSelect]) + return (
- { - if (geoData) { - const placeId = (geoData as { value: { place_id: string } }).value.place_id - const geocode = (await geocodeByPlaceId(placeId)).at(0) - - if (!geocode) { - return + {error ?
{error}
:
} + {geoData && ( + + )}
) } From c7a084e635f3134a27b5665447ce00dec3b3f6ec Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 19:49:35 +0000 Subject: [PATCH 02/14] docs(geocoding): add changelog entry for PlaceAutocompleteElement migration Also fix prettier config and lint-staged to handle .md files correctly (default parser was set to typescript, causing markdown formatting to fail). https://claude.ai/code/session_01TZQpaQ1MNq3SQ8uEU7iCSt --- .prettierrc.json | 6 ++++++ geocoding/CHANGELOG.md | 6 +++++- package.json | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index eb7b3c1e..4d7e90c9 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -10,6 +10,12 @@ "options": { "requirePragma": true } + }, + { + "files": "*.md", + "options": { + "parser": "markdown" + } } ] } diff --git a/geocoding/CHANGELOG.md b/geocoding/CHANGELOG.md index 353d73b4..ee8eb985 100644 --- a/geocoding/CHANGELOG.md +++ b/geocoding/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.3.0 + +- **BREAKING**: Replaced `react-google-places-autocomplete` with Google's native `PlaceAutocompleteElement` web component. This resolves the deprecation warning for `google.maps.places.AutocompleteService` (deprecated as of March 2025). The `react-google-places-autocomplete` dependency has been removed; the plugin now loads the Google Maps JS API directly and uses the recommended `PlaceAutocompleteElement` and `fetchFields` API. + ## 0.2.0 - **BREAKING**: The Google Maps API key is now a required plugin configuration option: @@ -12,7 +16,7 @@ plugins: [payloadGeocodingPlugin({})] plugins: [ payloadGeocodingPlugin({ googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, - }) + }), ] ``` diff --git a/package.json b/package.json index 92d2e104..2a07c55a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "lint-staged": { "*.{js,jsx,ts,tsx}": "prettier --write", "*.json": "prettier --write --parser json", - "*.{css,md,yaml,yml}": "prettier --write" + "*.md": "prettier --write --parser markdown", + "*.{css,yaml,yml}": "prettier --write" } } From 848e2041bc1b147ccd2a6117b510e332e159fae1 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:12:25 +0200 Subject: [PATCH 03/14] fix(geocoding): use ReactSelect with AutocompleteSuggestion API instead of PlaceAutocompleteElement --- geocoding/CHANGELOG.md | 1 + .../src/components/GeocodingFieldClient.tsx | 255 ++++++++++-------- 2 files changed, 140 insertions(+), 116 deletions(-) diff --git a/geocoding/CHANGELOG.md b/geocoding/CHANGELOG.md index ee8eb985..2f9123b6 100644 --- a/geocoding/CHANGELOG.md +++ b/geocoding/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.3.0 - **BREAKING**: Replaced `react-google-places-autocomplete` with Google's native `PlaceAutocompleteElement` web component. This resolves the deprecation warning for `google.maps.places.AutocompleteService` (deprecated as of March 2025). The `react-google-places-autocomplete` dependency has been removed; the plugin now loads the Google Maps JS API directly and uses the recommended `PlaceAutocompleteElement` and `fetchFields` API. +- **BREAKING**: The **Places API (New)** must be enabled in your Google Cloud project. The new `PlaceAutocompleteElement` uses the Places API (New), which is a separate API from the legacy Places API. Enable it at: https://console.developers.google.com/apis/api/places.googleapis.com/overview ## 0.2.0 diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 3636aee3..9f984cd4 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -1,8 +1,10 @@ 'use client' import type { FieldBaseClient } from 'payload' -import { FieldError, FieldLabel, useField } from '@payloadcms/ui' -import { useCallback, useEffect, useRef, useState } from 'react' +import { FieldError, FieldLabel, ReactSelect, useField } from '@payloadcms/ui' +import { useEffect, useRef, useState } from 'react' + +type SelectOption = { label: string; value: unknown } interface GeocodingFieldComponentProps { field: Pick @@ -11,41 +13,57 @@ interface GeocodingFieldComponentProps { } /** - * Loads the Google Maps JS API script if not already present. - * Returns a promise that resolves when the API is ready. + * Loads the Google Maps Places library using the bootstrap loader. */ -function loadGoogleMapsApi(apiKey: string): Promise { - return new Promise((resolve, reject) => { - if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { - resolve() - return - } - - const existingScript = document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]') - if (existingScript) { - existingScript.addEventListener('load', () => resolve()) - existingScript.addEventListener('error', () => - reject(new Error('Failed to load Google Maps API')), - ) - return +async function loadGoogleMapsPlaces(apiKey: string): Promise { + if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { + await google.maps.importLibrary('places') + return + } + + // Set up the bootstrap loader stub before loading the script + const w = window as unknown as { google: { maps: Record } } + const g = w.google || (w.google = {} as { maps: Record }) + const m = g.maps || (g.maps = {}) + + const libraries = new Set() + let loadPromise: Promise | null = null + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(m as any).importLibrary = (name: string) => { + libraries.add(name) + if (!loadPromise) { + loadPromise = new Promise((resolve, reject) => { + const script = document.createElement('script') + const params = new URLSearchParams({ + callback: '__gmcb', + key: apiKey, + libraries: [...libraries].join(','), + }) + script.src = `https://maps.googleapis.com/maps/api/js?${params}` + script.async = true + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(window as any).__gmcb = () => { + resolve() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).__gmcb + } + script.onerror = () => reject(new Error('Failed to load Google Maps API')) + document.head.appendChild(script) + }) } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return loadPromise.then(() => (google.maps as any).importLibrary(name)) + } - const script = document.createElement('script') - script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&libraries=places&loading=async` - script.async = true - script.defer = true - script.onload = () => resolve() - script.onerror = () => reject(new Error('Failed to load Google Maps API')) - document.head.appendChild(script) - }) + await google.maps.importLibrary('places') } /** - * A custom client component that shows the Google Places Autocomplete component and + * A custom client component that shows a Google Places Autocomplete dropdown and * fills the point and geodata fields with the received data from the Google Places API. * - * Uses the new PlaceAutocompleteElement (google.maps.places) instead of the deprecated - * AutocompleteService. + * Uses the new AutocompleteSuggestion API and Payload's ReactSelect component. */ export const GeocodingFieldClient = ({ field, @@ -59,85 +77,106 @@ export const GeocodingFieldClient = ({ }) const { setValue: setPoint } = useField>({ path: pointFieldPath }) - const containerRef = useRef(null) - const autocompleteRef = useRef(null) - const [error, setError] = useState(null) + const [options, setOptions] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [apiReady, setApiReady] = useState(false) + const [error, setError] = useState(null) + const sessionTokenRef = useRef(null) + const debounceRef = useRef | null>(null) + + useEffect(() => { + loadGoogleMapsPlaces(googleMapsApiKey) + .then(() => setApiReady(true)) + .catch((e) => setError(e instanceof Error ? e.message : 'Failed to load Google Maps')) + }, [googleMapsApiKey]) + + const handleInputChange = (inputValue: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current) + } + + if (!inputValue || inputValue.length < 2 || !apiReady) { + setOptions([]) + return + } - const handlePlaceSelect = useCallback( - async (event: Event) => { - const placeEvent = event as google.maps.places.PlaceAutocompletePlaceSelectEvent - const place = placeEvent.place + setIsLoading(true) - if (!place) { - return + debounceRef.current = setTimeout(async () => { + try { + if (!sessionTokenRef.current) { + sessionTokenRef.current = new google.maps.places.AutocompleteSessionToken() + } + + const { suggestions } = + await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ + input: inputValue, + sessionToken: sessionTokenRef.current, + }) + + setOptions( + suggestions + .filter((s) => s.placePrediction) + .map((s) => ({ + label: s.placePrediction!.text.text, + value: { + description: s.placePrediction!.text.text, + mainText: s.placePrediction!.mainText?.text, + placeId: s.placePrediction!.placeId, + secondaryText: s.placePrediction!.secondaryText?.text, + toPlace: () => s.placePrediction!.toPlace(), + }, + })), + ) + } catch { + setOptions([]) + } finally { + setIsLoading(false) } + }, 300) + } + + const handleChange = async (option: SelectOption | SelectOption[] | null | undefined) => { + if (!option || Array.isArray(option)) { + setPoint([]) + setGeoData(null) + return + } + + const placeData = option.value as { + description: string + mainText: string + placeId: string + toPlace: () => google.maps.places.Place + } + try { + const place = placeData.toPlace() await place.fetchFields({ fields: ['displayName', 'formattedAddress', 'location'], + sessionToken: sessionTokenRef.current!, }) + // Reset session token after fetchFields (per Google billing best practice) + sessionTokenRef.current = null + if (place.location) { setPoint([place.location.lng(), place.location.lat()]) setGeoData({ label: place.formattedAddress ?? place.displayName, value: { description: place.formattedAddress, - place_id: place.id, + place_id: placeData.placeId, structured_formatting: { main_text: place.displayName, }, }, }) } - }, - [setGeoData, setPoint], - ) - - useEffect(() => { - let cancelled = false - - const init = async () => { - try { - await loadGoogleMapsApi(googleMapsApiKey) - if (cancelled || !containerRef.current) { - return - } - - await google.maps.importLibrary('places') - if (cancelled || !containerRef.current) { - return - } - - // Don't re-create if already initialized - if (autocompleteRef.current) { - return - } - - const autocomplete = new google.maps.places.PlaceAutocompleteElement({}) - autocompleteRef.current = autocomplete - - // Style the input to fill the container - autocomplete.style.width = '100%' - - autocomplete.addEventListener('gmp-placeselect', handlePlaceSelect) - - containerRef.current.appendChild(autocomplete) - } catch (e) { - if (!cancelled) { - setError(e instanceof Error ? e.message : 'Failed to load Google Maps') - } - } - } - - void init() - - return () => { - cancelled = true - if (autocompleteRef.current) { - autocompleteRef.current.removeEventListener('gmp-placeselect', handlePlaceSelect) - } + } catch { + setError('Failed to fetch place details') } - }, [googleMapsApiKey, handlePlaceSelect]) + } return (
@@ -145,35 +184,19 @@ export const GeocodingFieldClient = ({
- {error ?
{error}
:
} - {geoData && ( - + {error ? ( +
{error}
+ ) : ( + handleChange(val as SelectOption | null)} + onInputChange={handleInputChange} + options={options} + placeholder="Search for a place..." + value={geoData as unknown as SelectOption} + /> )}
) From ca8b8c61f00fd446f733a4062f1ca3cc397a0889 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:20:02 +0200 Subject: [PATCH 04/14] refactor(geocoding): improve GeocodingFieldClient reliability and performance --- .../src/components/GeocodingFieldClient.tsx | 266 ++++++++++-------- geocoding/tsconfig.json | 1 + 2 files changed, 144 insertions(+), 123 deletions(-) diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 9f984cd4..76e08366 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -1,62 +1,66 @@ 'use client' +/// import type { FieldBaseClient } from 'payload' import { FieldError, FieldLabel, ReactSelect, useField } from '@payloadcms/ui' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' type SelectOption = { label: string; value: unknown } +interface PlaceOptionValue { + description: string + mainText: string + placeId: string + secondaryText: string +} + interface GeocodingFieldComponentProps { field: Pick googleMapsApiKey: string path: string } -/** - * Loads the Google Maps Places library using the bootstrap loader. - */ -async function loadGoogleMapsPlaces(apiKey: string): Promise { +// Module-level singleton for the Google Maps API loading +let mapsLoadPromise: null | Promise = null + +function loadGoogleMapsPlaces(apiKey: string): Promise { + if (mapsLoadPromise) { + return mapsLoadPromise + } + if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { - await google.maps.importLibrary('places') - return + mapsLoadPromise = google.maps.importLibrary('places').then(() => {}) + return mapsLoadPromise } - // Set up the bootstrap loader stub before loading the script - const w = window as unknown as { google: { maps: Record } } - const g = w.google || (w.google = {} as { maps: Record }) - const m = g.maps || (g.maps = {}) - - const libraries = new Set() - let loadPromise: Promise | null = null - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(m as any).importLibrary = (name: string) => { - libraries.add(name) - if (!loadPromise) { - loadPromise = new Promise((resolve, reject) => { - const script = document.createElement('script') - const params = new URLSearchParams({ - callback: '__gmcb', - key: apiKey, - libraries: [...libraries].join(','), - }) - script.src = `https://maps.googleapis.com/maps/api/js?${params}` - script.async = true - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).__gmcb = () => { - resolve() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window as any).__gmcb - } - script.onerror = () => reject(new Error('Failed to load Google Maps API')) - document.head.appendChild(script) - }) - } + mapsLoadPromise = new Promise((resolve, reject) => { + // Set up bootstrap loader stub so importLibrary is available after load + const w = window as unknown as { google: { maps: Record } } + const g = w.google || (w.google = {} as { maps: Record }) + g.maps || (g.maps = {}) + + const script = document.createElement('script') + const params = new URLSearchParams({ + callback: '__gmcb', + key: apiKey, + libraries: 'places', + }) + script.src = `https://maps.googleapis.com/maps/api/js?${params}` + script.async = true // eslint-disable-next-line @typescript-eslint/no-explicit-any - return loadPromise.then(() => (google.maps as any).importLibrary(name)) - } + ;(window as any).__gmcb = () => { + resolve() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).__gmcb + } + script.onerror = () => { + mapsLoadPromise = null + reject(new Error('Failed to load Google Maps API')) + } + document.head.appendChild(script) + }) - await google.maps.importLibrary('places') + return mapsLoadPromise } /** @@ -80,103 +84,119 @@ export const GeocodingFieldClient = ({ const [options, setOptions] = useState([]) const [isLoading, setIsLoading] = useState(false) const [apiReady, setApiReady] = useState(false) - const [error, setError] = useState(null) + const [error, setError] = useState(null) const sessionTokenRef = useRef(null) - const debounceRef = useRef | null>(null) + const debounceRef = useRef>(null) useEffect(() => { + let cancelled = false loadGoogleMapsPlaces(googleMapsApiKey) - .then(() => setApiReady(true)) - .catch((e) => setError(e instanceof Error ? e.message : 'Failed to load Google Maps')) - }, [googleMapsApiKey]) - - const handleInputChange = (inputValue: string) => { - if (debounceRef.current) { - clearTimeout(debounceRef.current) - } - - if (!inputValue || inputValue.length < 2 || !apiReady) { - setOptions([]) - return + .then(() => { + if (!cancelled) { + setApiReady(true) + } + }) + .catch((e) => { + if (!cancelled) { + setError(e instanceof Error ? e.message : 'Failed to load Google Maps') + } + }) + return () => { + cancelled = true + if (debounceRef.current) { + clearTimeout(debounceRef.current) + } } + }, [googleMapsApiKey]) - setIsLoading(true) - - debounceRef.current = setTimeout(async () => { - try { - if (!sessionTokenRef.current) { - sessionTokenRef.current = new google.maps.places.AutocompleteSessionToken() - } + const handleInputChange = useCallback( + (inputValue: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current) + } - const { suggestions } = - await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ - input: inputValue, - sessionToken: sessionTokenRef.current, - }) + if (!inputValue || inputValue.length < 2 || !apiReady) { + setOptions([]) + setIsLoading(false) + return + } - setOptions( - suggestions - .filter((s) => s.placePrediction) - .map((s) => ({ + debounceRef.current = setTimeout(async () => { + setIsLoading(true) + try { + if (!sessionTokenRef.current) { + sessionTokenRef.current = new google.maps.places.AutocompleteSessionToken() + } + + const { suggestions } = + await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ + input: inputValue, + sessionToken: sessionTokenRef.current, + }) + + const filtered = suggestions.filter((s) => s.placePrediction != null) + setOptions( + filtered.map((s) => ({ label: s.placePrediction!.text.text, value: { description: s.placePrediction!.text.text, - mainText: s.placePrediction!.mainText?.text, + mainText: s.placePrediction!.mainText?.text ?? '', placeId: s.placePrediction!.placeId, - secondaryText: s.placePrediction!.secondaryText?.text, - toPlace: () => s.placePrediction!.toPlace(), - }, + secondaryText: s.placePrediction!.secondaryText?.text ?? '', + } satisfies PlaceOptionValue, })), - ) - } catch { - setOptions([]) - } finally { - setIsLoading(false) - } - }, 300) - } - - const handleChange = async (option: SelectOption | SelectOption[] | null | undefined) => { - if (!option || Array.isArray(option)) { - setPoint([]) - setGeoData(null) - return - } - - const placeData = option.value as { - description: string - mainText: string - placeId: string - toPlace: () => google.maps.places.Place - } + ) + } catch { + setOptions([]) + } finally { + setIsLoading(false) + } + }, 300) + }, + [apiReady], + ) - try { - const place = placeData.toPlace() - await place.fetchFields({ - fields: ['displayName', 'formattedAddress', 'location'], - sessionToken: sessionTokenRef.current!, - }) + const handleChange = useCallback( + (option: null | SelectOption | SelectOption[]) => { + if (!option || Array.isArray(option)) { + setPoint([]) + setGeoData(null) + return + } - // Reset session token after fetchFields (per Google billing best practice) - sessionTokenRef.current = null - - if (place.location) { - setPoint([place.location.lng(), place.location.lat()]) - setGeoData({ - label: place.formattedAddress ?? place.displayName, - value: { - description: place.formattedAddress, - place_id: placeData.placeId, - structured_formatting: { - main_text: place.displayName, - }, - }, + const placeData = option.value as PlaceOptionValue + + const place = new google.maps.places.Place({ id: placeData.placeId }) + // sessionToken is supported at runtime but missing from @types/google.maps + place + .fetchFields({ + fields: ['displayName', 'formattedAddress', 'location'], + sessionToken: sessionTokenRef.current, + } as { sessionToken: unknown } & google.maps.places.FetchFieldsRequest) + .then(() => { + // Reset session token after fetchFields (per Google billing best practice) + sessionTokenRef.current = null + + if (place.location) { + setPoint([place.location.lng(), place.location.lat()]) + setGeoData({ + label: place.formattedAddress ?? place.displayName, + value: { + description: place.formattedAddress, + place_id: placeData.placeId, + structured_formatting: { + main_text: place.displayName, + }, + }, + }) + } }) - } - } catch { - setError('Failed to fetch place details') - } - } + .catch(() => { + setError('Failed to fetch place details') + }) + }, + [setGeoData, setPoint], + ) return (
@@ -191,7 +211,7 @@ export const GeocodingFieldClient = ({ isClearable isLoading={isLoading} isSearchable - onChange={(val) => handleChange(val as SelectOption | null)} + onChange={(val) => handleChange(val as null | SelectOption)} onInputChange={handleInputChange} options={options} placeholder="Search for a place..." diff --git a/geocoding/tsconfig.json b/geocoding/tsconfig.json index 169db5ed..a7a8eeb9 100644 --- a/geocoding/tsconfig.json +++ b/geocoding/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "baseUrl": ".", "lib": ["DOM", "DOM.Iterable", "ES2022"], + "typeRoots": ["./node_modules/@types", "./node_modules/.pnpm/node_modules/@types"], "rootDir": "./", "allowJs": true, "skipLibCheck": true, From bb4430fae8a75c70306a4a2ac709fe96524efa40 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:25:54 +0200 Subject: [PATCH 05/14] simplify geocoding field component --- .../src/components/GeocodingFieldClient.tsx | 221 ++++++------------ .../src/components/loadGoogleMapsPlaces.ts | 46 ++++ 2 files changed, 115 insertions(+), 152 deletions(-) create mode 100644 geocoding/src/components/loadGoogleMapsPlaces.ts diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 76e08366..2b3e4160 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -3,16 +3,11 @@ import type { FieldBaseClient } from 'payload' import { FieldError, FieldLabel, ReactSelect, useField } from '@payloadcms/ui' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' -type SelectOption = { label: string; value: unknown } +import { loadGoogleMapsPlaces } from './loadGoogleMapsPlaces.js' -interface PlaceOptionValue { - description: string - mainText: string - placeId: string - secondaryText: string -} +type SelectOption = { label: string; value: unknown } interface GeocodingFieldComponentProps { field: Pick @@ -20,55 +15,6 @@ interface GeocodingFieldComponentProps { path: string } -// Module-level singleton for the Google Maps API loading -let mapsLoadPromise: null | Promise = null - -function loadGoogleMapsPlaces(apiKey: string): Promise { - if (mapsLoadPromise) { - return mapsLoadPromise - } - - if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { - mapsLoadPromise = google.maps.importLibrary('places').then(() => {}) - return mapsLoadPromise - } - - mapsLoadPromise = new Promise((resolve, reject) => { - // Set up bootstrap loader stub so importLibrary is available after load - const w = window as unknown as { google: { maps: Record } } - const g = w.google || (w.google = {} as { maps: Record }) - g.maps || (g.maps = {}) - - const script = document.createElement('script') - const params = new URLSearchParams({ - callback: '__gmcb', - key: apiKey, - libraries: 'places', - }) - script.src = `https://maps.googleapis.com/maps/api/js?${params}` - script.async = true - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ;(window as any).__gmcb = () => { - resolve() - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window as any).__gmcb - } - script.onerror = () => { - mapsLoadPromise = null - reject(new Error('Failed to load Google Maps API')) - } - document.head.appendChild(script) - }) - - return mapsLoadPromise -} - -/** - * A custom client component that shows a Google Places Autocomplete dropdown and - * fills the point and geodata fields with the received data from the Google Places API. - * - * Uses the new AutocompleteSuggestion API and Payload's ReactSelect component. - */ export const GeocodingFieldClient = ({ field, googleMapsApiKey, @@ -76,31 +22,22 @@ export const GeocodingFieldClient = ({ }: GeocodingFieldComponentProps) => { const pointFieldPath = path.replace('_googlePlacesData', '') - const { setValue: setGeoData, value: geoData } = useField({ - path, - }) + const { setValue: setGeoData, value: geoData } = useField({ path }) const { setValue: setPoint } = useField>({ path: pointFieldPath }) const [options, setOptions] = useState([]) const [isLoading, setIsLoading] = useState(false) - const [apiReady, setApiReady] = useState(false) const [error, setError] = useState(null) const sessionTokenRef = useRef(null) const debounceRef = useRef>(null) useEffect(() => { let cancelled = false - loadGoogleMapsPlaces(googleMapsApiKey) - .then(() => { - if (!cancelled) { - setApiReady(true) - } - }) - .catch((e) => { - if (!cancelled) { - setError(e instanceof Error ? e.message : 'Failed to load Google Maps') - } - }) + loadGoogleMapsPlaces(googleMapsApiKey).catch((e) => { + if (!cancelled) { + setError(e instanceof Error ? e.message : 'Failed to load Google Maps') + } + }) return () => { cancelled = true if (debounceRef.current) { @@ -109,94 +46,74 @@ export const GeocodingFieldClient = ({ } }, [googleMapsApiKey]) - const handleInputChange = useCallback( - (inputValue: string) => { - if (debounceRef.current) { - clearTimeout(debounceRef.current) - } - - if (!inputValue || inputValue.length < 2 || !apiReady) { - setOptions([]) - setIsLoading(false) - return - } + const handleInputChange = (inputValue: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current) + } + if (!inputValue || inputValue.length < 2) { + setOptions([]) + setIsLoading(false) + return + } - debounceRef.current = setTimeout(async () => { - setIsLoading(true) - try { - if (!sessionTokenRef.current) { - sessionTokenRef.current = new google.maps.places.AutocompleteSessionToken() - } + debounceRef.current = setTimeout(async () => { + setIsLoading(true) + try { + if (!sessionTokenRef.current) { + sessionTokenRef.current = new google.maps.places.AutocompleteSessionToken() + } - const { suggestions } = - await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ - input: inputValue, - sessionToken: sessionTokenRef.current, - }) + const { suggestions } = + await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ + input: inputValue, + sessionToken: sessionTokenRef.current, + }) - const filtered = suggestions.filter((s) => s.placePrediction != null) - setOptions( - filtered.map((s) => ({ + setOptions( + suggestions + .filter((s) => s.placePrediction != null) + .map((s) => ({ label: s.placePrediction!.text.text, - value: { - description: s.placePrediction!.text.text, - mainText: s.placePrediction!.mainText?.text ?? '', - placeId: s.placePrediction!.placeId, - secondaryText: s.placePrediction!.secondaryText?.text ?? '', - } satisfies PlaceOptionValue, + value: s.placePrediction!.placeId, })), - ) - } catch { - setOptions([]) - } finally { - setIsLoading(false) - } - }, 300) - }, - [apiReady], - ) - - const handleChange = useCallback( - (option: null | SelectOption | SelectOption[]) => { - if (!option || Array.isArray(option)) { - setPoint([]) - setGeoData(null) - return + ) + } catch { + setOptions([]) + } finally { + setIsLoading(false) } + }, 300) + } - const placeData = option.value as PlaceOptionValue - - const place = new google.maps.places.Place({ id: placeData.placeId }) - // sessionToken is supported at runtime but missing from @types/google.maps - place - .fetchFields({ - fields: ['displayName', 'formattedAddress', 'location'], - sessionToken: sessionTokenRef.current, - } as { sessionToken: unknown } & google.maps.places.FetchFieldsRequest) - .then(() => { - // Reset session token after fetchFields (per Google billing best practice) - sessionTokenRef.current = null + const handleChange = (option: null | SelectOption | SelectOption[]) => { + if (!option || Array.isArray(option)) { + setPoint([]) + setGeoData(null) + return + } - if (place.location) { - setPoint([place.location.lng(), place.location.lat()]) - setGeoData({ - label: place.formattedAddress ?? place.displayName, - value: { - description: place.formattedAddress, - place_id: placeData.placeId, - structured_formatting: { - main_text: place.displayName, - }, - }, - }) - } - }) - .catch(() => { - setError('Failed to fetch place details') - }) - }, - [setGeoData, setPoint], - ) + const placeId = option.value as string + new google.maps.places.Place({ id: placeId }) + .fetchFields({ + fields: ['displayName', 'formattedAddress', 'location'], + sessionToken: sessionTokenRef.current, + } as { sessionToken: unknown } & google.maps.places.FetchFieldsRequest) + .then(({ place }) => { + sessionTokenRef.current = null + if (place.location) { + setPoint([place.location.lng(), place.location.lat()]) + setGeoData({ + label: place.formattedAddress ?? place.displayName, + value: { + description: place.formattedAddress, + place_id: placeId, + structured_formatting: { main_text: place.displayName }, + }, + }) + } + }) + .catch(() => setError('Failed to fetch place details')) + } return (
diff --git a/geocoding/src/components/loadGoogleMapsPlaces.ts b/geocoding/src/components/loadGoogleMapsPlaces.ts new file mode 100644 index 00000000..0f0d0eaa --- /dev/null +++ b/geocoding/src/components/loadGoogleMapsPlaces.ts @@ -0,0 +1,46 @@ +/// + +let mapsLoadPromise: null | Promise = null + +/** + * Loads the Google Maps Places library. Uses a module-level singleton + * so concurrent callers share the same load. Resets on error to allow retry. + */ +export function loadGoogleMapsPlaces(apiKey: string): Promise { + if (mapsLoadPromise) { + return mapsLoadPromise + } + + if (typeof google !== 'undefined' && typeof google.maps?.importLibrary === 'function') { + mapsLoadPromise = google.maps.importLibrary('places').then(() => {}) + return mapsLoadPromise + } + + mapsLoadPromise = new Promise((resolve, reject) => { + const w = window as unknown as { google: { maps: Record } } + const g = w.google || (w.google = {} as { maps: Record }) + g.maps || (g.maps = {}) + + const script = document.createElement('script') + const params = new URLSearchParams({ + callback: '__gmcb', + key: apiKey, + libraries: 'places', + }) + script.src = `https://maps.googleapis.com/maps/api/js?${params}` + script.async = true + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(window as any).__gmcb = () => { + resolve() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (window as any).__gmcb + } + script.onerror = () => { + mapsLoadPromise = null + reject(new Error('Failed to load Google Maps API')) + } + document.head.appendChild(script) + }) + + return mapsLoadPromise +} From d1b13d79628e89b29ab8d979252afd7943db9d41 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:50:15 +0200 Subject: [PATCH 06/14] refactor(geocoding): rename fields, flatten stored data shape, add migration script --- geocoding/CHANGELOG.md | 6 +- geocoding/README.md | 20 ++- geocoding/dev/src/collection/pages.ts | 6 +- geocoding/migrations/migrate-to-0.3.0.ts | 149 ++++++++++++++++++ .../src/components/GeocodingFieldClient.tsx | 39 +++-- geocoding/src/fields/geocodingField.ts | 31 +++- geocoding/src/types/GeoCodingFieldConfig.ts | 2 +- 7 files changed, 218 insertions(+), 35 deletions(-) create mode 100644 geocoding/migrations/migrate-to-0.3.0.ts diff --git a/geocoding/CHANGELOG.md b/geocoding/CHANGELOG.md index 2f9123b6..bf408016 100644 --- a/geocoding/CHANGELOG.md +++ b/geocoding/CHANGELOG.md @@ -2,8 +2,10 @@ ## 0.3.0 -- **BREAKING**: Replaced `react-google-places-autocomplete` with Google's native `PlaceAutocompleteElement` web component. This resolves the deprecation warning for `google.maps.places.AutocompleteService` (deprecated as of March 2025). The `react-google-places-autocomplete` dependency has been removed; the plugin now loads the Google Maps JS API directly and uses the recommended `PlaceAutocompleteElement` and `fetchFields` API. -- **BREAKING**: The **Places API (New)** must be enabled in your Google Cloud project. The new `PlaceAutocompleteElement` uses the Places API (New), which is a separate API from the legacy Places API. Enable it at: https://console.developers.google.com/apis/api/places.googleapis.com/overview +- **BREAKING**: Replaced `react-google-places-autocomplete` with the Google Maps Places API (New) `AutocompleteSuggestion` API. This resolves the deprecation warning for `google.maps.places.AutocompleteService` (deprecated as of March 2025). The `react-google-places-autocomplete` dependency has been removed. +- **BREAKING**: The **Places API (New)** must be enabled in your Google Cloud project. Enable it at: https://console.developers.google.com/apis/api/places.googleapis.com/overview +- **BREAKING**: The `_googlePlacesData` JSON field has been renamed to `_meta` and now stores a flat `{ name, formattedAddress, googlePlaceId, types }` object instead of the previous `react-google-places-autocomplete` format. Existing documents will need to be migrated — see [`migrations/migrate-to-0.3.0.ts`](./migrations/migrate-to-0.3.0.ts). +- **BREAKING**: The `geoDataFieldOverride` config option has been renamed to `locationMetaOverride`. ## 0.2.0 diff --git a/geocoding/README.md b/geocoding/README.md index ab9922e5..7f97ee0a 100644 --- a/geocoding/README.md +++ b/geocoding/README.md @@ -25,11 +25,11 @@ plugins: [ To use this plugin, you'll need a Google Maps API key. To get one, follow these steps: 1. Set up a Google Cloud account and create a project -2. Enable the Maps JavaScript API in your Google Cloud project -3. Create an API key with `Maps JavaScript API`, `Places API` and `Geocoding API` access +2. Enable the **Maps JavaScript API** and the **Places API (New)** in your Google Cloud project +3. Create an API key with access to these APIs 4. Pass the API key to the plugin configuration (as shown above). -Note: Since this API key is exposed to the frontend (Payload Admin panel), it is strongly recommended to restrict its usage by setting up domain restrictions and only enabling the `Maps JavaScript API`, `Places API`, and `Geocoding API` in the Google Cloud Console under API Keys & Credentials +Note: Since this API key is exposed to the frontend (Payload Admin panel), it is strongly recommended to restrict its usage by setting up domain restrictions in the Google Cloud Console under API Keys & Credentials. ## Usage @@ -44,9 +44,9 @@ geocodingField({ }) ``` -This will add a `location_googlePlacesData` JSON field to the collection. This field will store the raw geocoding data from the Google Places API. +This will add a `location_meta` JSON field to the collection that stores metadata about the selected location (display name, formatted address, and Google Place ID (`googlePlaceId`)). -If needed you can adjust the `location_googlePlacesData` field name by passing a `name` property to the `geocodingField` function. +You can customize the metadata field by passing a `locationMetaOverride` option: ```ts geocodingField({ @@ -54,8 +54,8 @@ geocodingField({ name: 'location', type: 'point', }, - geoDataFieldOverride: { - label: 'JSON Geodata Field', + locationMetaOverride: { + label: 'Location Metadata', access: { read: () => true, update: () => true, @@ -63,7 +63,6 @@ geocodingField({ }, admin: { readOnly: false, - description: 'This field stores the geocoding data from the Google Places API.', }, }, }), @@ -71,14 +70,13 @@ geocodingField({ ## About this plugin -This plugin uses the [react-google-places-autocomplete](https://www.npmjs.com/package/react-google-places-autocomplete) library to provide a Select/Search input for finding an address. The result of the Google Places API request is stored in a JSON field and the coordinates are stored in a Point Field. +This plugin uses the Google Maps Places API (New) `AutocompleteSuggestion` API to provide a search input for finding an address. The selected location's metadata is stored in a JSON field and the coordinates are stored in a Point Field. ## Roadmap > ⚠️ **Warning**: This plugin is actively evolving and may undergo significant changes. While it is functional, please thoroughly test before using in production environments. -- Use the native Payload `SelectField` instead of the field provided by `react-google-places-autocomplete` -- Extend the field config to accept `GooglePlacesAutocomplete` options like debounce time, API options, etc. +- Extend the field config to accept options like debounce time, API options, etc. - Add support for other geocoding services (Mapbox, HERE, etc.) Have a suggestion for the plugin? Any feedback is welcome! diff --git a/geocoding/dev/src/collection/pages.ts b/geocoding/dev/src/collection/pages.ts index aed49d53..1ee4f7ea 100644 --- a/geocoding/dev/src/collection/pages.ts +++ b/geocoding/dev/src/collection/pages.ts @@ -50,18 +50,18 @@ export const Pages: CollectionConfig = { type: 'point', required: true, }, - geoDataFieldOverride: { + locationMetaOverride: { required: true, }, }), - // With geoDataFieldOverride + // With locationMetaOverride geocodingField({ pointField: { name: 'location3', type: 'point', }, - geoDataFieldOverride: { + locationMetaOverride: { label: 'Geodata (Custom label)', access: { read: () => true, diff --git a/geocoding/migrations/migrate-to-0.3.0.ts b/geocoding/migrations/migrate-to-0.3.0.ts new file mode 100644 index 00000000..b87865c4 --- /dev/null +++ b/geocoding/migrations/migrate-to-0.3.0.ts @@ -0,0 +1,149 @@ +/** + * Migration script for @jhb.software/payload-geocoding-plugin v0.3.0 + * + * This migration handles: + * 1. Renaming `_googlePlacesData` fields to `_meta` + * 2. Transforming the stored data shape from the old react-google-places-autocomplete + * format to the new flat format + * + * Old format (v0.2.x): + * { label: string, value: { description: string, place_id: string, structured_formatting: { main_text: string } } } + * + * New format (v0.3.0): + * { displayName: string, formattedAddress: string, googlePlaceId: string } + * + * Usage: + * 1. Copy this file into your project + * 2. Update COLLECTIONS_TO_MIGRATE with your collection slugs and point field names + * 3. Run with: npx tsx migrations/migrate-to-0.3.0.ts + * + * Alternatively, integrate the `migrateCollection` function into a Payload migration. + */ + +import type { BasePayload, CollectionSlug } from 'payload' + +// ===================================================================== +// CONFIGURE THESE: Add your collection slugs and point field names +// ===================================================================== +const COLLECTIONS_TO_MIGRATE: { collection: CollectionSlug; pointFieldName: string }[] = [ + // Example: + // { collection: 'pages', pointFieldName: 'location' }, + // { collection: 'events', pointFieldName: 'venue' }, +] + +interface OldGeoData { + label?: string + value?: { + description?: string + place_id?: string + structured_formatting?: { + main_text?: string + } + } +} + +interface NewLocationMeta { + formattedAddress: string + googlePlaceId: string + name: string + types: string[] +} + +function transformGeoData(old: OldGeoData): NewLocationMeta | null { + if (!old?.value?.place_id) { + return null + } + + return { + formattedAddress: old.value.description ?? old.label ?? '', + googlePlaceId: old.value.place_id, + name: old.value.structured_formatting?.main_text ?? old.value.description ?? '', + types: [], // types were not stored in the old format + } +} + +export async function migrateCollection( + payload: BasePayload, + collection: CollectionSlug, + pointFieldName: string, +): Promise<{ migrated: number; skipped: number }> { + const oldFieldName = `${pointFieldName}_googlePlacesData` + const newFieldName = `${pointFieldName}_meta` + + let migrated = 0 + let skipped = 0 + let hasMore = true + let page = 1 + + while (hasMore) { + const result = await payload.find({ + collection, + depth: 0, + limit: 100, + page, + select: { [oldFieldName]: true }, + where: { + [oldFieldName]: { exists: true }, + }, + }) + + for (const doc of result.docs) { + const oldData = (doc as Record)[oldFieldName] as OldGeoData | undefined + if (!oldData) { + skipped++ + continue + } + + const newData = transformGeoData(oldData) + if (!newData) { + skipped++ + continue + } + + await payload.update({ + id: doc.id, + collection, + data: { [newFieldName]: newData }, + }) + + migrated++ + } + + hasMore = result.hasNextPage + page++ + } + + return { migrated, skipped } +} + +/** + * Run this script standalone with: npx tsx migrations/migrate-to-0.3.0.ts + */ +async function main() { + if (COLLECTIONS_TO_MIGRATE.length === 0) { + console.error('No collections configured. Edit COLLECTIONS_TO_MIGRATE in this file.') + process.exit(1) + } + + // Import your Payload config — adjust the path as needed + const { getPayload } = await import('payload') + + const payload = await getPayload({ + // Adjust this import path to point to your payload.config.ts + config: (await import('../payload.config')).default, + }) + + for (const { collection, pointFieldName } of COLLECTIONS_TO_MIGRATE) { + console.log(`Migrating ${collection}.${pointFieldName}...`) + const result = await migrateCollection(payload, collection, pointFieldName) + console.log(` Done: ${result.migrated} migrated, ${result.skipped} skipped`) + } + + console.log('Migration complete.') + process.exit(0) +} + +main().catch((err) => { + console.error('Migration failed:', err) + process.exit(1) +}) diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 2b3e4160..f73f4dc2 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -9,20 +9,37 @@ import { loadGoogleMapsPlaces } from './loadGoogleMapsPlaces.js' type SelectOption = { label: string; value: unknown } +interface LocationMeta { + formattedAddress: string + googlePlaceId: string + name: string + types: string[] +} + interface GeocodingFieldComponentProps { field: Pick googleMapsApiKey: string path: string } +function toSelectOption(meta: LocationMeta): SelectOption { + const label = + meta.name && meta.formattedAddress && !meta.formattedAddress.startsWith(meta.name) + ? `${meta.name}, ${meta.formattedAddress}` + : meta.formattedAddress || meta.name + return { label, value: meta.googlePlaceId } +} + export const GeocodingFieldClient = ({ field, googleMapsApiKey, path, }: GeocodingFieldComponentProps) => { - const pointFieldPath = path.replace('_googlePlacesData', '') + const pointFieldPath = path.replace('_meta', '') - const { setValue: setGeoData, value: geoData } = useField({ path }) + const { setValue: setLocationMeta, value: locationMeta } = useField({ + path, + }) const { setValue: setPoint } = useField>({ path: pointFieldPath }) const [options, setOptions] = useState([]) @@ -88,27 +105,25 @@ export const GeocodingFieldClient = ({ const handleChange = (option: null | SelectOption | SelectOption[]) => { if (!option || Array.isArray(option)) { setPoint([]) - setGeoData(null) + setLocationMeta(null) return } const placeId = option.value as string new google.maps.places.Place({ id: placeId }) .fetchFields({ - fields: ['displayName', 'formattedAddress', 'location'], + fields: ['displayName', 'formattedAddress', 'location', 'types'], sessionToken: sessionTokenRef.current, } as { sessionToken: unknown } & google.maps.places.FetchFieldsRequest) .then(({ place }) => { sessionTokenRef.current = null if (place.location) { setPoint([place.location.lng(), place.location.lat()]) - setGeoData({ - label: place.formattedAddress ?? place.displayName, - value: { - description: place.formattedAddress, - place_id: placeId, - structured_formatting: { main_text: place.displayName }, - }, + setLocationMeta({ + formattedAddress: place.formattedAddress, + googlePlaceId: placeId, + name: place.displayName, + types: place.types ?? [], }) } }) @@ -132,7 +147,7 @@ export const GeocodingFieldClient = ({ onInputChange={handleInputChange} options={options} placeholder="Search for a place..." - value={geoData as unknown as SelectOption} + value={locationMeta ? toSelectOption(locationMeta) : undefined} /> )}
diff --git a/geocoding/src/fields/geocodingField.ts b/geocoding/src/fields/geocodingField.ts index 44e81f46..277c2725 100644 --- a/geocoding/src/fields/geocodingField.ts +++ b/geocoding/src/fields/geocodingField.ts @@ -5,7 +5,7 @@ import type { GeoCodingFieldConfig } from '../types/GeoCodingFieldConfig.js' /** * Creates a row field containing: * 1. The provided point field for storing the coordinates from the Google Places API - * 2. A JSON field that stores the raw Google Places API geocoding data + * 2. A JSON field that stores location metadata (display name, address, googlePlaceId) */ export const geocodingField = (config: GeoCodingFieldConfig): Field => { return { @@ -15,22 +15,41 @@ export const geocodingField = (config: GeoCodingFieldConfig): Field => { }, fields: [ { - name: config.pointField.name + '_googlePlacesData', + name: config.pointField.name + '_meta', type: 'json', - access: config.geoDataFieldOverride?.access ?? {}, + access: config.locationMetaOverride?.access ?? {}, admin: { // overridable props: readOnly: true, - ...config.geoDataFieldOverride?.admin, + ...config.locationMetaOverride?.admin, // non-overridable props: components: { Field: '@jhb.software/payload-geocoding-plugin/server#GeocodingField', }, }, - label: config.geoDataFieldOverride?.label ?? 'Location', - required: config.geoDataFieldOverride?.required, + jsonSchema: { + fileMatch: ['a://b/foo.json'], + schema: { + type: 'object', + additionalProperties: false, + properties: { + formattedAddress: { description: 'Full formatted address', type: 'string' }, + googlePlaceId: { description: 'Google Places ID', type: 'string' }, + name: { description: 'Place or business name', type: 'string' }, + types: { + description: 'Place types (e.g. locality, political)', + items: { type: 'string' }, + type: 'array', + }, + }, + required: ['formattedAddress', 'googlePlaceId', 'name', 'types'], + }, + uri: 'a://b/foo.json', + }, + label: config.locationMetaOverride?.label ?? 'Location', + required: config.locationMetaOverride?.required, }, config.pointField, ], diff --git a/geocoding/src/types/GeoCodingFieldConfig.ts b/geocoding/src/types/GeoCodingFieldConfig.ts index 1f44a8ed..b99c2620 100644 --- a/geocoding/src/types/GeoCodingFieldConfig.ts +++ b/geocoding/src/types/GeoCodingFieldConfig.ts @@ -2,7 +2,7 @@ import type { JSONField, PointField } from 'payload' /** Configuration for the geocoding fields. */ export type GeoCodingFieldConfig = { - geoDataFieldOverride?: { + locationMetaOverride?: { access?: JSONField['access'] admin?: JSONField['admin'] label?: string From b2647c5246350c697285660255b62957acae2f11 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:50:32 +0200 Subject: [PATCH 07/14] chore(geocoding): regenerate payload types --- geocoding/dev/src/payload-types.ts | 203 +++++++++++++++++++---------- 1 file changed, 133 insertions(+), 70 deletions(-) diff --git a/geocoding/dev/src/payload-types.ts b/geocoding/dev/src/payload-types.ts index bb5f3bda..950d6637 100644 --- a/geocoding/dev/src/payload-types.ts +++ b/geocoding/dev/src/payload-types.ts @@ -149,86 +149,140 @@ export interface User { export interface Page { id: string; title?: string | null; - location_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 */ location?: [number, number] | null; - location0_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location0_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 */ location0?: [number, number] | null; - location1_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location1_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 */ location1: [number, number]; - location2_googlePlacesData: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location2_meta: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 */ location2: [number, number]; - location3_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location3_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 */ location3?: [number, number] | null; locationGroup?: { - location_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 @@ -237,15 +291,24 @@ export interface Page { }; locations?: | { - location_googlePlacesData?: - | { - [k: string]: unknown; - } - | unknown[] - | string - | number - | boolean - | null; + location_meta?: { + /** + * Full formatted address + */ + formattedAddress: string; + /** + * Google Places ID + */ + googlePlaceId: string; + /** + * Place or business name + */ + name: string; + /** + * Place types (e.g. locality, political) + */ + types: string[]; + }; /** * @minItems 2 * @maxItems 2 @@ -359,26 +422,26 @@ export interface UsersSelect { */ export interface PagesSelect { title?: T; - location_googlePlacesData?: T; + location_meta?: T; location?: T; - location0_googlePlacesData?: T; + location0_meta?: T; location0?: T; - location1_googlePlacesData?: T; + location1_meta?: T; location1?: T; - location2_googlePlacesData?: T; + location2_meta?: T; location2?: T; - location3_googlePlacesData?: T; + location3_meta?: T; location3?: T; locationGroup?: | T | { - location_googlePlacesData?: T; + location_meta?: T; location?: T; }; locations?: | T | { - location_googlePlacesData?: T; + location_meta?: T; location?: T; id?: T; }; From 112e276531dcd8dd7da8f4ec6f2d870304e63039 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:57:53 +0200 Subject: [PATCH 08/14] style(geocoding): fix lint errors --- geocoding/src/components/GeocodingFieldClient.tsx | 2 +- geocoding/src/components/loadGoogleMapsPlaces.ts | 4 +++- geocoding/src/fields/geocodingField.ts | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index f73f4dc2..2c7638fc 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -120,9 +120,9 @@ export const GeocodingFieldClient = ({ if (place.location) { setPoint([place.location.lng(), place.location.lat()]) setLocationMeta({ + name: place.displayName, formattedAddress: place.formattedAddress, googlePlaceId: placeId, - name: place.displayName, types: place.types ?? [], }) } diff --git a/geocoding/src/components/loadGoogleMapsPlaces.ts b/geocoding/src/components/loadGoogleMapsPlaces.ts index 0f0d0eaa..287a53fa 100644 --- a/geocoding/src/components/loadGoogleMapsPlaces.ts +++ b/geocoding/src/components/loadGoogleMapsPlaces.ts @@ -19,7 +19,9 @@ export function loadGoogleMapsPlaces(apiKey: string): Promise { mapsLoadPromise = new Promise((resolve, reject) => { const w = window as unknown as { google: { maps: Record } } const g = w.google || (w.google = {} as { maps: Record }) - g.maps || (g.maps = {}) + if (!g.maps) { + g.maps = {} + } const script = document.createElement('script') const params = new URLSearchParams({ diff --git a/geocoding/src/fields/geocodingField.ts b/geocoding/src/fields/geocodingField.ts index 277c2725..4df65ce5 100644 --- a/geocoding/src/fields/geocodingField.ts +++ b/geocoding/src/fields/geocodingField.ts @@ -35,13 +35,13 @@ export const geocodingField = (config: GeoCodingFieldConfig): Field => { type: 'object', additionalProperties: false, properties: { - formattedAddress: { description: 'Full formatted address', type: 'string' }, - googlePlaceId: { description: 'Google Places ID', type: 'string' }, - name: { description: 'Place or business name', type: 'string' }, + name: { type: 'string', description: 'Place or business name' }, + formattedAddress: { type: 'string', description: 'Full formatted address' }, + googlePlaceId: { type: 'string', description: 'Google Places ID' }, types: { + type: 'array', description: 'Place types (e.g. locality, political)', items: { type: 'string' }, - type: 'array', }, }, required: ['formattedAddress', 'googlePlaceId', 'name', 'types'], From 7858f8d71b06fd9771a1e3a8c94c53d30188743a Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:59:13 +0200 Subject: [PATCH 09/14] fix(geocoding): remove descriptions from json schema to reduce generated types --- geocoding/src/fields/geocodingField.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/geocoding/src/fields/geocodingField.ts b/geocoding/src/fields/geocodingField.ts index 4df65ce5..046ec233 100644 --- a/geocoding/src/fields/geocodingField.ts +++ b/geocoding/src/fields/geocodingField.ts @@ -35,14 +35,10 @@ export const geocodingField = (config: GeoCodingFieldConfig): Field => { type: 'object', additionalProperties: false, properties: { - name: { type: 'string', description: 'Place or business name' }, - formattedAddress: { type: 'string', description: 'Full formatted address' }, - googlePlaceId: { type: 'string', description: 'Google Places ID' }, - types: { - type: 'array', - description: 'Place types (e.g. locality, political)', - items: { type: 'string' }, - }, + name: { type: 'string' }, + formattedAddress: { type: 'string' }, + googlePlaceId: { type: 'string' }, + types: { type: 'array', items: { type: 'string' } }, }, required: ['formattedAddress', 'googlePlaceId', 'name', 'types'], }, From 8e146a8c5995604e3e58f5fb3699311fad7456f8 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 22:59:46 +0200 Subject: [PATCH 10/14] chore(geocoding): regenerate payload types --- geocoding/dev/src/payload-types.ts | 98 +++--------------------------- 1 file changed, 7 insertions(+), 91 deletions(-) diff --git a/geocoding/dev/src/payload-types.ts b/geocoding/dev/src/payload-types.ts index 950d6637..9b488a54 100644 --- a/geocoding/dev/src/payload-types.ts +++ b/geocoding/dev/src/payload-types.ts @@ -150,21 +150,9 @@ export interface Page { id: string; title?: string | null; location_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -173,21 +161,9 @@ export interface Page { */ location?: [number, number] | null; location0_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -196,21 +172,9 @@ export interface Page { */ location0?: [number, number] | null; location1_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -219,21 +183,9 @@ export interface Page { */ location1: [number, number]; location2_meta: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -242,21 +194,9 @@ export interface Page { */ location2: [number, number]; location3_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -266,21 +206,9 @@ export interface Page { location3?: [number, number] | null; locationGroup?: { location_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** @@ -292,21 +220,9 @@ export interface Page { locations?: | { location_meta?: { - /** - * Full formatted address - */ + name: string; formattedAddress: string; - /** - * Google Places ID - */ googlePlaceId: string; - /** - * Place or business name - */ - name: string; - /** - * Place types (e.g. locality, political) - */ types: string[]; }; /** From 96b7175555fd974b04080914274b871658a4d175 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 23:09:09 +0200 Subject: [PATCH 11/14] fix(geocoding): disable client-side filtering in ReactSelect for server-side search --- geocoding/src/components/GeocodingFieldClient.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 2c7638fc..7a795d4b 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -140,6 +140,7 @@ export const GeocodingFieldClient = ({
{error}
) : ( true} isClearable isLoading={isLoading} isSearchable From bb31d14358fcbe6397d3b3a116de0aca9ea7c3a2 Mon Sep 17 00:00:00 2001 From: Jens Becker Date: Mon, 6 Apr 2026 23:10:36 +0200 Subject: [PATCH 12/14] fix(geocoding): use null instead of empty array when clearing point field --- geocoding/src/components/GeocodingFieldClient.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geocoding/src/components/GeocodingFieldClient.tsx b/geocoding/src/components/GeocodingFieldClient.tsx index 7a795d4b..3809efec 100644 --- a/geocoding/src/components/GeocodingFieldClient.tsx +++ b/geocoding/src/components/GeocodingFieldClient.tsx @@ -104,7 +104,7 @@ export const GeocodingFieldClient = ({ const handleChange = (option: null | SelectOption | SelectOption[]) => { if (!option || Array.isArray(option)) { - setPoint([]) + setPoint(null) setLocationMeta(null) return } From dce365afef647bed527d55297cb4e04a5c60d399 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 07:59:50 +0000 Subject: [PATCH 13/14] feat(geocoding): add server-side geocoding endpoint and hooks for agent/API usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two server-side geocoding mechanisms for AI agents and API consumers: - GET /api/geocoding-plugin/search?q=
— authenticated endpoint with custom access function support - {field}_address virtual field with beforeChange hooks — auto-geocodes address strings on create/update, populates point + _meta fields Uses the new _meta field shape (name, formattedAddress, googlePlaceId, types). Both hooks share a single API call via req.context promise caching. Also adds: - Full test infrastructure: 27 unit tests + 10 integration tests (SQLite) - Articles collection with Lexical block for testing geocoding in blocks - test-geocoding CI job - "Usage with AI Agents / API" README section https://claude.ai/code/session_01Ja3JVC48ozET22Z7yJkyY6 --- .github/workflows/ci.yml | 34 + geocoding/README.md | 69 + geocoding/dev/.gitignore | 3 +- geocoding/dev/package.json | 16 +- geocoding/dev/plugin.spec.ts | 30 - geocoding/dev/plugin.test.ts | 328 ++ geocoding/dev/src/collection/articles.ts | 49 + geocoding/dev/src/payload.config.ts | 12 +- .../dev/src/test/generateDatabaseAdapter.ts | 57 + geocoding/dev/src/test/vitest.setup.ts | 6 + geocoding/dev/vite.config.ts | 18 + geocoding/package.json | 7 +- geocoding/pnpm-lock.yaml | 2744 ++++++++++++++++- .../src/endpoints/geocodingSearch.test.ts | 104 + geocoding/src/endpoints/geocodingSearch.ts | 45 + geocoding/src/fields/geocodingField.test.ts | 59 + geocoding/src/fields/geocodingField.ts | 108 +- .../src/hooks/geocodeBeforeChange.test.ts | 150 + geocoding/src/hooks/geocodeBeforeChange.ts | 112 + geocoding/src/index.ts | 7 +- geocoding/src/plugin.test.ts | 60 + geocoding/src/plugin.ts | 8 + .../src/services/googleGeocoding.test.ts | 82 + geocoding/src/services/googleGeocoding.ts | 69 + geocoding/src/types/GeoCodingPluginConfig.ts | 14 + 25 files changed, 4067 insertions(+), 124 deletions(-) delete mode 100644 geocoding/dev/plugin.spec.ts create mode 100644 geocoding/dev/plugin.test.ts create mode 100644 geocoding/dev/src/collection/articles.ts create mode 100644 geocoding/dev/src/test/generateDatabaseAdapter.ts create mode 100644 geocoding/dev/src/test/vitest.setup.ts create mode 100644 geocoding/dev/vite.config.ts create mode 100644 geocoding/src/endpoints/geocodingSearch.test.ts create mode 100644 geocoding/src/endpoints/geocodingSearch.ts create mode 100644 geocoding/src/fields/geocodingField.test.ts create mode 100644 geocoding/src/hooks/geocodeBeforeChange.test.ts create mode 100644 geocoding/src/hooks/geocodeBeforeChange.ts create mode 100644 geocoding/src/plugin.test.ts create mode 100644 geocoding/src/services/googleGeocoding.test.ts create mode 100644 geocoding/src/services/googleGeocoding.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 864f9837..6b062a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,40 @@ jobs: - name: Run ESLint (astro-payload-richtext-lexical) run: cd astro-payload-richtext-lexical && pnpm lint + test-geocoding: + runs-on: ubuntu-latest + needs: format + name: test-geocoding + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup environment variables + run: | + echo "PAYLOAD_SECRET=test-secret-not-for-production" > geocoding/dev/.env + echo "SQLITE_URL=file:./payload-test.db" >> geocoding/dev/.env + echo "NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=test-api-key" >> geocoding/dev/.env + + - name: Setup pnpm + uses: pnpm/action-setup@v5 + with: + version: ^9.0.0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install:all + + - name: Run tests + run: cd geocoding && pnpm test:sqlite + env: + PAYLOAD_DATABASE: sqlite + test-pages-localized: runs-on: ubuntu-latest needs: format diff --git a/geocoding/README.md b/geocoding/README.md index 7f97ee0a..98cf222f 100644 --- a/geocoding/README.md +++ b/geocoding/README.md @@ -68,6 +68,75 @@ geocodingField({ }), ``` +## Usage with AI Agents / API + +The default UI-based autocomplete requires a browser, which makes it unusable for AI agents and other API consumers. The plugin provides two server-side mechanisms to solve this. + +### Geocoding Search Endpoint + +The plugin registers a `GET /api/geocoding-plugin/search` endpoint that geocodes addresses server-side. It is authenticated by default (requires a logged-in user), and supports a custom access function: + +```ts +plugins: [ + payloadGeocodingPlugin({ + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, + // Optional: customize who can access the endpoint + geocodingEndpoint: { + access: ({ req }) => Boolean(req.user), + }, + }), +] +``` + +An agent can then search for locations and use the results to populate fields: + +```bash +# 1. Search for an address +GET /api/geocoding-plugin/search?q=Alexanderplatz,+Berlin + +# Response: +{ + "results": [ + { + "name": "Alexanderplatz", + "formattedAddress": "Alexanderplatz, 10178 Berlin, Germany", + "googlePlaceId": "ChIJp1l4uWBRqEcR2SPNRBMhtAI", + "location": { "lat": 52.5219, "lng": 13.4132 }, + "types": [...] + } + ] +} + +# 2. Use the result to create/update a document +POST /api/pages +{ + "title": "My Page", + "location": [13.4132, 52.5219], + "location_meta": { + "name": "Alexanderplatz", + "formattedAddress": "Alexanderplatz, 10178 Berlin, Germany", + "googlePlaceId": "ChIJp1l4uWBRqEcR2SPNRBMhtAI", + "types": ["point_of_interest", "establishment"] + } +} +``` + +### Server-Side Address Geocoding (beforeChange Hook) + +Every `geocodingField` automatically includes a hidden `{fieldName}_address` text field. When an address string is submitted via the API, a `beforeChange` hook geocodes it server-side and populates the point and meta fields. + +An agent can simply submit an address string — the coordinates and metadata are resolved automatically: + +```bash +POST /api/pages +{ + "title": "My Page", + "location_address": "Alexanderplatz, Berlin" +} +``` + +The hook geocodes the address, sets the `location` point field to `[lng, lat]`, and populates `location_meta` with the location metadata. + ## About this plugin This plugin uses the Google Maps Places API (New) `AutocompleteSuggestion` API to provide a search input for finding an address. The selected location's metadata is stored in a JSON field and the coordinates are stored in a Point Field. diff --git a/geocoding/dev/.gitignore b/geocoding/dev/.gitignore index 60259b83..5ebf18be 100644 --- a/geocoding/dev/.gitignore +++ b/geocoding/dev/.gitignore @@ -370,4 +370,5 @@ $RECYCLE.BIN/ /media *.db tsconfig.tsbuildinfo -/dist \ No newline at end of file +/dist +src/test/databaseAdapter.ts \ No newline at end of file diff --git a/geocoding/dev/package.json b/geocoding/dev/package.json index a6f34bbc..d9646117 100644 --- a/geocoding/dev/package.json +++ b/geocoding/dev/package.json @@ -1,6 +1,6 @@ { - "name": "payload-plugin-test-app", - "description": "A test app for the plugin", + "name": "payload-geocoding-plugin-test-app", + "description": "A test app for the geocoding plugin", "version": "0.0.1", "license": "MIT", "type": "module", @@ -12,14 +12,19 @@ "start": "cross-env NODE_OPTIONS=--no-deprecation next start", "format": "prettier --write src", "payload": "payload", - "generate:types": "payload generate:types", + "generate:types": "cross-env PAYLOAD_DATABASE=sqlite payload generate:types", "generate:schema": "payload-graphql generate:schema", - "generate:importmap": "payload generate:importmap" + "generate:importmap": "payload generate:importmap", + "test": "pnpm test:sqlite", + "test:sqlite": "cross-env PAYLOAD_DATABASE=sqlite vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@jhb.software/payload-geocoding-plugin": "workspace:*", "@payloadcms/db-mongodb": "^3.81.0", + "@payloadcms/db-sqlite": "^3.81.0", "@payloadcms/next": "^3.81.0", + "@payloadcms/richtext-lexical": "3.81.0", "@payloadcms/ui": "^3.81.0", "next": "15.4.11", "payload": "^3.81.0", @@ -30,6 +35,7 @@ "copyfiles": "^2.4.1", "cross-env": "^10.1.0", "dotenv": "^17.4.1", - "typescript": "5.9.3" + "vite": "^8.0.0", + "vitest": "^4.1.0" } } diff --git a/geocoding/dev/plugin.spec.ts b/geocoding/dev/plugin.spec.ts deleted file mode 100644 index 9391d33c..00000000 --- a/geocoding/dev/plugin.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Server } from 'http' -import mongoose from 'mongoose' -import payload from 'payload' -import { start } from './src/server' - -describe('Plugin tests', () => { - let server: Server - - beforeAll(async () => { - await start({ local: true }) - }) - - afterAll(async () => { - await mongoose.connection.dropDatabase() - await mongoose.connection.close() - server.close() - }) - - // Add tests to ensure that the plugin works as expected - - // Example test to check for seeded data - it('seeds data accordingly', async () => { - const newCollectionQuery = await payload.find({ - collection: 'new-collection', - sort: 'createdAt', - }) - - expect(newCollectionQuery.totalDocs).toEqual(1) - }) -}) diff --git a/geocoding/dev/plugin.test.ts b/geocoding/dev/plugin.test.ts new file mode 100644 index 00000000..7051db6b --- /dev/null +++ b/geocoding/dev/plugin.test.ts @@ -0,0 +1,328 @@ +import payload, { type CollectionSlug, type SanitizedConfig } from 'payload' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest' +import config from './src/payload.config' + +const MOCK_GOOGLE_RESPONSE = { + results: [ + { + address_components: [ + { long_name: 'Alexanderplatz', short_name: 'Alexanderplatz', types: ['point_of_interest'] }, + { long_name: 'Berlin', short_name: 'Berlin', types: ['locality'] }, + ], + formatted_address: 'Alexanderplatz, 10178 Berlin, Germany', + geometry: { + location: { lat: 52.5219, lng: 13.4132 }, + }, + place_id: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + types: ['point_of_interest', 'establishment'], + }, + ], + status: 'OK', +} + +/** + * The Pages collection has `location1` (required point) and `location2` (required point + required meta). + * These must be provided in every create call to satisfy validation. + */ +const requiredFields = { + location1: [0, 0] as [number, number], + location2: [0, 0] as [number, number], + location2_meta: { name: 'Test', formattedAddress: 'Test', googlePlaceId: 'test', types: ['test'] }, +} + +/** Returns a fresh mock Response for each call (avoids "Body already read" errors). */ +function mockGoogleGeocodingFetch() { + return vi.spyOn(globalThis, 'fetch').mockImplementation(async () => { + return new Response(JSON.stringify(MOCK_GOOGLE_RESPONSE), { status: 200 }) + }) +} + +beforeAll(async () => { + await payload.init({ + config: config, + }) + + await deleteAllCollections(config, ['users']) +}) + +afterAll(async () => { + await deleteAllCollections(config) + + if (payload.db && typeof payload.db.destroy === 'function') { + await payload.db.destroy() + } +}) + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('Geocoding plugin field structure', () => { + beforeEach(async () => await deleteCollection('pages')) + + test('creates a page with point and meta fields via the local API', async () => { + const meta = { name: 'Berlin', formattedAddress: 'Berlin, Germany', googlePlaceId: 'abc', types: ['locality'] } + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Test Page', + location: [13.4132, 52.5219], + location_meta: meta, + ...requiredFields, + }, + }) + + expect(page.location).toEqual([13.4132, 52.5219]) + expect(page.location_meta).toEqual(meta) + }) + + test('creates a page with only the required fields', async () => { + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Minimal Page', + ...requiredFields, + }, + }) + + expect(page.title).toBe('Minimal Page') + expect(page.location).toBeUndefined() + }) + + test('geocoding fields work inside a group', async () => { + const meta = { name: 'Place', formattedAddress: 'Somewhere', googlePlaceId: 'xyz', types: ['locality'] } + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Group Test', + locationGroup: { + location: [10.0, 50.0], + location_meta: meta, + }, + ...requiredFields, + }, + }) + + expect(page.locationGroup?.location).toEqual([10.0, 50.0]) + expect(page.locationGroup?.location_meta).toEqual(meta) + }) + + test('geocoding fields work inside an array', async () => { + const metaA = { name: 'A', formattedAddress: 'Place A', googlePlaceId: 'a', types: ['locality'] } + const metaB = { name: 'B', formattedAddress: 'Place B', googlePlaceId: 'b', types: ['locality'] } + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Array Test', + locations: [ + { location: [8.0, 48.0], location_meta: metaA }, + { location: [9.0, 49.0], location_meta: metaB }, + ], + ...requiredFields, + }, + }) + + expect(page.locations).toHaveLength(2) + expect(page.locations![0].location).toEqual([8.0, 48.0]) + expect(page.locations![1].location).toEqual([9.0, 49.0]) + }) +}) + +describe('Server-side address geocoding (beforeChange hook)', () => { + beforeEach(async () => await deleteCollection('pages')) + + test('auto-geocodes an address string submitted via location_address', async () => { + mockGoogleGeocodingFetch() + + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Geocoded Page', + location_address: 'Alexanderplatz, Berlin', + ...requiredFields, + }, + }) + + // Point field should be populated with [lng, lat] + expect(page.location).toEqual([13.4132, 52.5219]) + + // Meta field should contain the geocoding result in LocationMeta shape + expect(page.location_meta).toMatchObject({ + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + googlePlaceId: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + name: 'Alexanderplatz', + }) + + // The address field should NOT be persisted (virtual: true) + const fetched = await payload.findByID({ + collection: 'pages', + id: page.id, + }) + expect((fetched as Record).location_address).toBeFalsy() + }) + + test('does not geocode when no address is provided', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch') + + await payload.create({ + collection: 'pages', + data: { + title: 'No Address Page', + location: [1.0, 2.0], + ...requiredFields, + }, + }) + + expect(fetchSpy).not.toHaveBeenCalled() + }) + + test('auto-geocodes address on update', async () => { + const page = await payload.create({ + collection: 'pages', + data: { + title: 'Update Test', + location: [0, 0], + ...requiredFields, + }, + }) + + mockGoogleGeocodingFetch() + + const updated = await payload.update({ + collection: 'pages', + id: page.id, + data: { + location_address: 'Alexanderplatz, Berlin', + }, + }) + + expect(updated.location).toEqual([13.4132, 52.5219]) + expect(updated.location_meta).toMatchObject({ + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + }) + }) +}) + +describe('Geocoding field inside a Lexical block', () => { + beforeEach(async () => await deleteCollection('articles')) + + test('creates an article with a locationBlock containing point and meta', async () => { + const article = await payload.create({ + collection: 'articles', + data: { + title: 'Article with Location Block', + content: { + root: { + type: 'root', + version: 1, + direction: 'ltr', + children: [ + { + type: 'block', + version: 1, + fields: { + id: 'loc-block-1', + blockName: 'Berlin Office', + blockType: 'locationBlock', + label: 'Berlin HQ', + location: [13.4132, 52.5219], + location_meta: { name: 'Berlin', formattedAddress: 'Berlin, Germany', googlePlaceId: 'abc', types: ['locality'] }, + }, + }, + ], + }, + }, + }, + }) + + const block = (article.content?.root?.children as any[])?.[0] + expect(block).toBeDefined() + expect(block.fields.blockType).toBe('locationBlock') + expect(block.fields.location).toEqual([13.4132, 52.5219]) + expect(block.fields.location_meta).toMatchObject({ name: 'Berlin' }) + }) + + test('auto-geocodes an address inside a Lexical block', async () => { + mockGoogleGeocodingFetch() + + const article = await payload.create({ + collection: 'articles', + data: { + title: 'Geocoded Block Article', + content: { + root: { + type: 'root', + version: 1, + direction: 'ltr', + children: [ + { + type: 'block', + version: 1, + fields: { + id: 'loc-block-2', + blockName: 'Auto-geocoded Location', + blockType: 'locationBlock', + label: 'Berlin Office', + location_address: 'Alexanderplatz, Berlin', + }, + }, + ], + }, + }, + }, + }) + + const block = (article.content?.root?.children as any[])?.[0] + expect(block).toBeDefined() + + expect(block.fields.location).toEqual([13.4132, 52.5219]) + expect(block.fields.location_meta).toMatchObject({ + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + googlePlaceId: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + }) + + // Known Payload bug: virtual fields inside Lexical blocks are still stored in the block's JSON. + // The address field has virtual: true, which correctly prevents a DB column for regular fields, + // but Lexical blocks serialize all field data as JSON and ignore the virtual flag. + // expect(block.fields.location_address).toBeFalsy() + }) +}) + +describe('Geocoding search endpoint', () => { + test('is registered at /api/geocoding-plugin/search', () => { + const endpoints = payload.config.endpoints + const geocodingEndpoint = endpoints?.find((e) => e.path === '/geocoding-plugin/search') + + expect(geocodingEndpoint).toBeDefined() + expect(geocodingEndpoint!.method).toBe('get') + }) +}) + +// --- Helpers --- + +const deleteCollection = async (collection: CollectionSlug) => { + await payload.db.deleteMany({ + collection: collection, + where: {}, + }) + + try { + await payload.db.deleteVersions({ + collection: collection, + where: {}, + }) + } catch {} +} + +const deleteAllCollections = async ( + config: Promise, + except: CollectionSlug[] = [], +) => { + const collections = (await config).collections?.filter((c) => !except.includes(c.slug)) ?? [] + + for (const collection of collections) { + if (!except.includes(collection.slug)) { + await deleteCollection(collection.slug) + } + } +} diff --git a/geocoding/dev/src/collection/articles.ts b/geocoding/dev/src/collection/articles.ts new file mode 100644 index 00000000..d10b9037 --- /dev/null +++ b/geocoding/dev/src/collection/articles.ts @@ -0,0 +1,49 @@ +import { geocodingField } from '../../../src/fields/geocodingField' + +import type { CollectionConfig } from 'payload' + +import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical' + +/** + * A collection for testing the geocoding field inside a Lexical editor block. + */ +export const Articles: CollectionConfig = { + slug: 'articles', + admin: { + useAsTitle: 'title', + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'content', + type: 'richText', + editor: lexicalEditor({ + features: [ + BlocksFeature({ + blocks: [ + { + slug: 'locationBlock', + fields: [ + { + name: 'label', + type: 'text', + }, + geocodingField({ + pointField: { + name: 'location', + type: 'point', + }, + }), + ], + }, + ], + }), + ], + }), + }, + ], +} diff --git a/geocoding/dev/src/payload.config.ts b/geocoding/dev/src/payload.config.ts index 0e585564..988a1347 100644 --- a/geocoding/dev/src/payload.config.ts +++ b/geocoding/dev/src/payload.config.ts @@ -1,10 +1,11 @@ import { payloadGeocodingPlugin } from '@jhb.software/payload-geocoding-plugin' -import { mongooseAdapter } from '@payloadcms/db-mongodb' import path from 'path' import { buildConfig } from 'payload' import { fileURLToPath } from 'url' +import { Articles } from './collection/articles' import { Pages } from './collection/pages' import { testEmailAdapter } from './emailAdapter' +import { databaseAdapter } from './test/databaseAdapter' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -24,18 +25,17 @@ export default buildConfig({ fields: [], }, Pages, + Articles, ], - db: mongooseAdapter({ - url: process.env.DATABASE_URI!, - }), + db: databaseAdapter, email: testEmailAdapter, - secret: process.env.PAYLOAD_SECRET!, + secret: process.env.PAYLOAD_SECRET || 'test-secret-for-ci', typescript: { outputFile: path.resolve(dirname, 'payload-types.ts'), }, plugins: [ payloadGeocodingPlugin({ - googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, + googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY || 'test-api-key', }), ], async onInit(payload) { diff --git a/geocoding/dev/src/test/generateDatabaseAdapter.ts b/geocoding/dev/src/test/generateDatabaseAdapter.ts new file mode 100644 index 00000000..51a000a0 --- /dev/null +++ b/geocoding/dev/src/test/generateDatabaseAdapter.ts @@ -0,0 +1,57 @@ +// NOTE: this pattern is inspired by https://github.com/payloadcms/payload/blob/main/test/generateDatabaseAdapter.ts + +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export type DatabaseAdapter = 'mongodb' | 'sqlite' + +const databaseAdapters: Record = { + mongodb: ` +import { mongooseAdapter } from '@payloadcms/db-mongodb' + +export const databaseAdapter = mongooseAdapter({ + url: process.env.MONGODB_URL!, +}) +`, + sqlite: ` +import { sqliteAdapter } from '@payloadcms/db-sqlite' + +export const databaseAdapter = sqliteAdapter({ + client: { + url: process.env.SQLITE_URL!, + }, +}) +`, +} + +/** + * Generates a database adapter file based on the PAYLOAD_DATABASE environment variable. + * This allows the same tests to run against different databases without code duplication. + */ +export function generateDatabaseAdapter(dbAdapter: DatabaseAdapter = 'mongodb'): string { + const adapterCode = databaseAdapters[dbAdapter] + if (!adapterCode) { + throw new Error( + `Unknown database adapter: ${dbAdapter}. Valid options are: ${Object.keys(databaseAdapters).join(', ')}`, + ) + } + + const outputPath = path.resolve(dirname, 'databaseAdapter.ts') + + fs.writeFileSync( + outputPath, + `// DO NOT MODIFY. This file is automatically generated. +// Generated for database: ${dbAdapter} +${adapterCode} +`, + ) + + console.log( + `\n##############################\n## Generated database adapter for: ${dbAdapter} ##\n##############################\n`, + ) + return adapterCode +} diff --git a/geocoding/dev/src/test/vitest.setup.ts b/geocoding/dev/src/test/vitest.setup.ts new file mode 100644 index 00000000..0d085cc2 --- /dev/null +++ b/geocoding/dev/src/test/vitest.setup.ts @@ -0,0 +1,6 @@ +import { generateDatabaseAdapter, type DatabaseAdapter } from './generateDatabaseAdapter.js' + +// Default to SQLite if no database specified +const dbAdapter = (process.env.PAYLOAD_DATABASE as DatabaseAdapter) || 'sqlite' + +generateDatabaseAdapter(dbAdapter) diff --git a/geocoding/dev/vite.config.ts b/geocoding/dev/vite.config.ts new file mode 100644 index 00000000..43184db0 --- /dev/null +++ b/geocoding/dev/vite.config.ts @@ -0,0 +1,18 @@ +import path from 'path' +import { loadEnv } from 'vite' +import { fileURLToPath } from 'url' +import { defineConfig } from 'vitest/config' + +const filename = fileURLToPath(import.meta.url) +const dirname = path.dirname(filename) + +export default defineConfig(({ mode }) => { + return { + test: { + env: loadEnv(mode, process.cwd(), ''), // Load environment variables + hookTimeout: 30000, // Increase hook timeout to 30 seconds + testTimeout: 30000, // Increase test timeout to 30 seconds + setupFiles: [path.resolve(dirname, 'src/test/vitest.setup.ts')], + }, + } +}) diff --git a/geocoding/package.json b/geocoding/package.json index b1142b80..cf61e563 100644 --- a/geocoding/package.json +++ b/geocoding/package.json @@ -22,7 +22,9 @@ "lint": "eslint src", "lint:fix": "eslint src --fix", "prepack": "pnpm prepublishOnly", - "prepublishOnly": "pnpm build && pnpm copyfiles" + "prepublishOnly": "pnpm build && pnpm copyfiles", + "test": "cd dev && pnpm test", + "test:sqlite": "cd dev && pnpm test:sqlite" }, "peerDependencies": { "@payloadcms/ui": "^3.81.0", @@ -42,7 +44,8 @@ "eslint": "^9.0.0", "prettier": "^3.8.1", "rimraf": "6.1.3", - "typescript": "5.9.3" + "typescript": "5.9.3", + "vitest": "^4.1.2" }, "publishConfig": { "main": "./dist/index.js", diff --git a/geocoding/pnpm-lock.yaml b/geocoding/pnpm-lock.yaml index 957a441b..cb705691 100644 --- a/geocoding/pnpm-lock.yaml +++ b/geocoding/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: typescript: specifier: 5.9.3 version: 5.9.3 + vitest: + specifier: ^4.1.2 + version: 4.1.2(happy-dom@20.8.9)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0)) dev: dependencies: @@ -66,9 +69,15 @@ importers: '@payloadcms/db-mongodb': specifier: ^3.81.0 version: 3.81.0(@aws-sdk/credential-providers@3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0)))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(socks@2.8.3) + '@payloadcms/db-sqlite': + specifier: ^3.81.0 + version: 3.81.0(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3)) '@payloadcms/next': specifier: ^3.81.0 version: 3.81.0(@types/react@19.2.14)(graphql@16.9.0)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@payloadcms/richtext-lexical': + specifier: 3.81.0 + version: 3.81.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.81.0(@types/react@19.2.14)(graphql@16.9.0)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@types/react@19.2.14)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(yjs@13.6.30) '@payloadcms/ui': specifier: ^3.81.0 version: 3.81.0(@types/react@19.2.14)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) @@ -94,9 +103,12 @@ importers: dotenv: specifier: ^17.4.1 version: 17.4.1 - typescript: - specifier: 5.9.3 - version: 5.9.3 + vite: + specifier: ^8.0.0 + version: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0) + vitest: + specifier: ^4.1.0 + version: 4.1.2(happy-dom@20.8.9)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0)) packages: @@ -304,9 +316,18 @@ packages: peerDependencies: react: '>=16.8.0' + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -351,156 +372,452 @@ packages: '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@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.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + 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.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + 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.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.3': resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + 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.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.3': resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + 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.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + 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.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + 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.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + 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.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + 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.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.3': resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.3': resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@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.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@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.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + 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.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@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.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.3': resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.3': resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + 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.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.3': resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} @@ -613,6 +930,18 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/react@0.27.19': + resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + '@floating-ui/react@0.27.3': resolution: {integrity: sha512-CLHnes3ixIFFKVQDdICjel8muhFLOBdQH7fgtHNPY8UbCNqbeKZ262G7K66lGQOUQWWnYocf7ZbUsLJgGfsLHg==} peerDependencies: @@ -806,6 +1135,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -815,6 +1147,134 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lexical/clipboard@0.41.0': + resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} + + '@lexical/code@0.41.0': + resolution: {integrity: sha512-0hoNi1KC9/N3SBOGcOcFqnT0OpwmcRRAhfxTKMGqfCtCvAMzULVwZ8RWc9/NV9bKYESgBTW5D9xkDANP2mspHg==} + + '@lexical/devtools-core@0.41.0': + resolution: {integrity: sha512-FzJtluBhBc8bKS11TUZe72KoZN/hnzIyiiM0SPJAsPwGpoXuM01jqpXQGybWf/1bWB+bmmhOae7O4Nywi/Csuw==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/dragon@0.41.0': + resolution: {integrity: sha512-gBEqkk8Q6ZPruvDaRcOdF1EK9suCVBODzOCcR+EnoJTaTjfDkCM7pkPAm4w90Wa1wCZEtFHvCfas+jU9MDSumg==} + + '@lexical/extension@0.41.0': + resolution: {integrity: sha512-sF4SPiP72yXvIGchmmIZ7Yg2XZTxNLOpFEIIzdqG7X/1fa1Ham9P/T7VbrblWpF6Ei5LJtK9JgNVB0hb4l3o1g==} + + '@lexical/hashtag@0.41.0': + resolution: {integrity: sha512-tFWM74RW4KU0E/sj2aowfWl26vmLUTp331CgVESnhQKcZBfT40KJYd57HEqBDTfQKn4MUhylQCCA0hbpw6EeFQ==} + + '@lexical/headless@0.41.0': + resolution: {integrity: sha512-MH8oDuUKdM/Jq0c9vlEEkCL9pEQg4SwyrABBGIbFf+87VBJ5EWDdG9g1vJq7fKSDxfhFux7F5+i+zgUnxOQR/g==} + + '@lexical/history@0.41.0': + resolution: {integrity: sha512-kGoVWsiOn62+RMjRolRa+NXZl8jFwxav6GNDiHH8yzivtoaH8n1SwUfLJELXCzeqzs81HySqD4q30VLJVTGoDg==} + + '@lexical/html@0.41.0': + resolution: {integrity: sha512-3RyZy+H/IDKz2D66rNN/NqYx87xVFrngfEbyu1OWtbY963RUFnopiVHCQvsge/8kT04QSZ7U/DzjVFqeNS6clg==} + + '@lexical/link@0.41.0': + resolution: {integrity: sha512-Rjtx5cGWAkKcnacncbVsZ1TqRnUB2Wm4eEVKpaAEG41+kHgqghzM2P+UGT15yROroxJu8KvAC9ISiYFiU4XE1w==} + + '@lexical/list@0.41.0': + resolution: {integrity: sha512-RXvB+xcbzVoQLGRDOBRCacztG7V+bI95tdoTwl8pz5xvgPtAaRnkZWMDP+yMNzMJZsqEChdtpxbf0NgtMkun6g==} + + '@lexical/mark@0.41.0': + resolution: {integrity: sha512-UO5WVs9uJAYIKHSlYh4Z1gHrBBchTOi21UCYBIZ7eAs4suK84hPzD+3/LAX5CB7ZltL6ke5Sly3FOwNXv/wfpA==} + + '@lexical/markdown@0.41.0': + resolution: {integrity: sha512-bzI73JMXpjGFhqUWNV6KqfjWcgAWzwFT+J3RHtbCF5rysC8HLldBYojOgAAtPfXqfxyv2mDzsY7SoJ75s9uHZA==} + + '@lexical/offset@0.41.0': + resolution: {integrity: sha512-2RHBXZqC8gm3X9C0AyRb0M8w7zJu5dKiasrif+jSKzsxPjAUeF1m95OtIOsWs1XLNUgASOSUqGovDZxKJslZfA==} + + '@lexical/overflow@0.41.0': + resolution: {integrity: sha512-Iy6ZiJip8X14EBYt1zKPOrXyQ4eG9JLBEoPoSVBTiSbVd+lYicdUvaOThT0k0/qeVTN9nqTaEltBjm56IrVKCQ==} + + '@lexical/plain-text@0.41.0': + resolution: {integrity: sha512-HIsGgmFUYRUNNyvckun33UQfU7LRzDlxymHUq67+Bxd5bXqdZOrStEKJXuDX+LuLh/GXZbaWNbDLqwLBObfbQg==} + + '@lexical/react@0.41.0': + resolution: {integrity: sha512-7+GUdZUm6sofWm+zdsWAs6cFBwKNsvsHezZTrf6k8jrZxL461ZQmbz/16b4DvjCGL9r5P1fR7md9/LCmk8TiCg==} + peerDependencies: + react: '>=17.x' + react-dom: '>=17.x' + + '@lexical/rich-text@0.41.0': + resolution: {integrity: sha512-yUcr7ZaaVTZNi8bow4CK1M8jy2qyyls1Vr+5dVjwBclVShOL/F/nFyzBOSb6RtXXRbd3Ahuk9fEleppX/RNIdw==} + + '@lexical/selection@0.41.0': + resolution: {integrity: sha512-1s7/kNyRzcv5uaTwsUL28NpiisqTf5xZ1zNukLsCN1xY+TWbv9RE9OxIv+748wMm4pxNczQe/UbIBODkbeknLw==} + + '@lexical/table@0.41.0': + resolution: {integrity: sha512-d3SPThBAr+oZ8O74TXU0iXM3rLbrAVC7/HcOnSAq7/AhWQW8yMutT51JQGN+0fMLP9kqoWSAojNtkdvzXfU/+A==} + + '@lexical/text@0.41.0': + resolution: {integrity: sha512-gGA+Anc7ck110EXo4KVKtq6Ui3M7Vz3OpGJ4QE6zJHWW8nV5h273koUGSutAMeoZgRVb6t01Izh3ORoFt/j1CA==} + + '@lexical/utils@0.41.0': + resolution: {integrity: sha512-Wlsokr5NQCq83D+7kxZ9qs5yQ3dU3Qaf2M+uXxLRoPoDaXqW8xTWZq1+ZFoEzsHzx06QoPa4Vu/40BZR91uQPg==} + + '@lexical/yjs@0.41.0': + resolution: {integrity: sha512-PaKTxSbVC4fpqUjQ7vUL9RkNF1PjL8TFl5jRe03PqoPYpE33buf3VXX6+cOUEfv9+uknSqLCPHoBS/4jN3a97w==} + peerDependencies: + yjs: '>=13.5.22' + + '@libsql/client@0.14.0': + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} + + '@libsql/core@0.14.0': + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + + '@libsql/darwin-arm64@0.4.7': + resolution: {integrity: sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.4.7': + resolution: {integrity: sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm64-gnu@0.4.7': + resolution: {integrity: sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.4.7': + resolution: {integrity: sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.4.7': + resolution: {integrity: sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.4.7': + resolution: {integrity: sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.4.7': + resolution: {integrity: sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==} + cpu: [x64] + os: [win32] + '@monaco-editor/loader@1.5.0': resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==} @@ -941,6 +1401,15 @@ packages: resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} engines: {node: '>= 10'} + '@napi-rs/wasm-runtime@1.1.2': + resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@next/env@15.4.11': resolution: {integrity: sha512-mIYp/091eYfPFezKX7ZPTWqrmSXq+ih6+LcUyKvLmeLQGhlPtot33kuEOd4U+xAA7sFfj21+OtCpIZx0g5SpvQ==} @@ -1011,11 +1480,24 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@payloadcms/db-mongodb@3.81.0': resolution: {integrity: sha512-6joKE/ytLOOuTtVc6N7qxbCLIbkEyR5Oi8CNVE0vqMoxBDz9/lLzTcm/9zpI9vzo0z+StOpytq9zx+GcxUOdGA==} peerDependencies: payload: 3.81.0 + '@payloadcms/db-sqlite@3.81.0': + resolution: {integrity: sha512-TgRUhl1mlPIa5O0Gw1caaXtfF319bZ798oRFNG0sU5vroR1PZIh3Cm7Wjvht/kAryu5lAVJo6V4aEerftGkxUw==} + peerDependencies: + payload: 3.81.0 + + '@payloadcms/drizzle@3.81.0': + resolution: {integrity: sha512-0hheoLFbxk1piSLtMaiTUc34HOhS3GOMnGw2hYJkYckrFMTjcJjM2nB4IEbRmk94CueS4LGitSt8xu7XwdZbkA==} + peerDependencies: + payload: 3.81.0 + '@payloadcms/eslint-config@3.28.0': resolution: {integrity: sha512-BiGtowdT4uLdGaM1yxP3oZRZrRMi27FiIU2eJuRqiGqdCVfKYRtlNAHq5knf2ExmdV/U3yZivH4linR2DNT2eA==} @@ -1037,6 +1519,17 @@ packages: next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.0-canary.10 <17.0.0' payload: 3.81.0 + '@payloadcms/richtext-lexical@3.81.0': + resolution: {integrity: sha512-qpAiWb2M9iARp8H/C1ewi9WNyTSpwId9IGFfHTDuU1oDL8Ke53sY9svUG+L7BViEuxKivWaUwfdo35RIrwPNQA==} + engines: {node: ^18.20.2 || >=20.9.0} + peerDependencies: + '@faceless-ui/modal': 3.0.0 + '@faceless-ui/scroll-info': 2.0.0 + '@payloadcms/next': 3.81.0 + payload: 3.81.0 + react: ^19.0.1 || ^19.1.2 || ^19.2.1 + react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1 + '@payloadcms/translations@3.81.0': resolution: {integrity: sha512-ruFKoYrTErl3Va5rOAyKn7Txh7wr6ZRw/KUZdj7YPOjI4LQpbtWYl3IQtcnS36x674GjeJOeWuEZJ50Xs3ofrQ==} @@ -1052,6 +1545,107 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@preact/signals-core@1.14.1': + resolution: {integrity: sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -1230,6 +1824,9 @@ packages: resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} engines: {node: '>=16.0.0'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@swc/cli@0.8.1': resolution: {integrity: sha512-L+ACCGHCiS0VqHVep/INLVnvRvJ2XooQFLZq4L8snhxw1jsqz+XRcY313UsyPVturPPE1shW3jic7rt3qEQTSQ==} engines: {node: '>= 20.19.0'} @@ -1344,21 +1941,42 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/busboy@1.5.4': resolution: {integrity: sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/google.maps@3.58.1': resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} @@ -1368,6 +1986,12 @@ packages: '@types/lodash@4.17.13': resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} @@ -1387,12 +2011,27 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.26.1': resolution: {integrity: sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1484,6 +2123,35 @@ packages: resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@4.1.2': + resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + + '@vitest/mocker@4.1.2': + resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.2': + resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + + '@vitest/runner@4.1.2': + resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + + '@vitest/snapshot@4.1.2': + resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + + '@vitest/spy@4.1.2': + resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + + '@vitest/utils@4.1.2': + resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} + '@xhmikosr/archive-type@8.0.1': resolution: {integrity: sha512-toXuiWChyfOpEiCPsIw6HGHaNji5LVkvB6EREL548vGWr+hGaehwxG4LzN20vm9aGFXwnA/Jty8yW2/SmV+1zQ==} engines: {node: '>=20'} @@ -1582,6 +2250,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -1680,6 +2352,9 @@ packages: buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1718,10 +2393,29 @@ packages: caniuse-lite@1.0.30001760: resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} @@ -1788,6 +2482,9 @@ packages: convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + copyfiles@2.4.1: resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==} hasBin: true @@ -1818,12 +2515,19 @@ packages: cssfilter@0.0.10: resolution: {integrity: sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -1865,6 +2569,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decompress-response@10.0.0: resolution: {integrity: sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==} engines: {node: '>=20'} @@ -1892,10 +2599,17 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -1907,6 +2621,102 @@ packages: resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} engines: {node: '>=12'} + drizzle-kit@0.31.7: + resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} + hasBin: true + + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1924,6 +2734,10 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} @@ -1939,6 +2753,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1955,6 +2772,21 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.27.3: resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} @@ -1964,6 +2796,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2148,6 +2983,15 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2163,6 +3007,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} engines: {node: '>=0.10.0'} @@ -2212,6 +3060,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -2265,6 +3117,10 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2386,6 +3242,10 @@ packages: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + happy-dom@20.8.9: + resolution: {integrity: sha512-Tz23LR9T9jOGVZm2x1EPdXqwA37G/owYMxRwU0E4miurAtFsPMQ1d2Jc2okUaSjZqAFz2oEn3FLXC5a0a+siyA==} + engines: {node: '>=20.0.0'} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2483,6 +3343,12 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2525,6 +3391,9 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2545,6 +3414,9 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -2633,6 +3505,9 @@ packages: resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} engines: {node: '>=20'} + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -2640,6 +3515,9 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-base64@3.7.8: + resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2679,6 +3557,10 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsox@1.2.121: + resolution: {integrity: sha512-9Ag50tKhpTwS6r5wh3MJSAvpSof0UBr39Pto8OnzFT32Z/pAbxAsKHzyvsyMEHVslELvHyO/4/jaQELHk8wDcw==} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -2704,13 +3586,100 @@ packages: language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lexical@0.41.0: + resolution: {integrity: sha512-pNIm5+n+hVnJHB9gYPDYsIO5Y59dNaDU9rJmPPsfqQhP2ojKFnUoPbcRnrI9FJLXB14sSumcY8LUw7Sq70TZqA==} + + lib0@0.2.117: + resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} + engines: {node: '>=16'} + hasBin: true + + libsql@0.4.7: + resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + cpu: [x64, arm64, wasm32] + os: [darwin, linux, win32] + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2725,6 +3694,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2737,6 +3709,9 @@ packages: resolution: {integrity: sha512-sr8xPKE25m6vJVcrdn6NxtC0fVfuPowbscLypegRgOm0yXSqr5JNHCAY3hnusdJ7HRBW04j6Ip4khvHU778DuQ==} engines: {node: 20 || >=22} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-asynchronous@1.1.0: resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} engines: {node: '>=18'} @@ -2748,6 +3723,21 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-mdx-jsx@3.1.3: + resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -2761,6 +3751,78 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-mdx-jsx@3.0.1: + resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2852,6 +3914,11 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2884,10 +3951,19 @@ packages: sass: optional: true + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + noms@0.0.0: resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==} @@ -2938,6 +4014,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -2984,6 +4063,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -3022,6 +4104,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + payload@3.81.0: resolution: {integrity: sha512-vnJYPTZQ54BuB3ZPNYSeaggPyvIye016DO+xAxDSlEocK2Gh7UZOFqtws2+G+0z7hIedLN57+/idqevQRFhcuA==} engines: {node: ^18.20.2 || >=20.9.0} @@ -3072,6 +4157,10 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3085,12 +4174,19 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3134,6 +4230,16 @@ packages: peerDependencies: react: ^19.2.4 + react-error-boundary@4.1.2: + resolution: {integrity: sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==} + peerDependencies: + react: '>=16.13.1' + + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-image-crop@10.1.8: resolution: {integrity: sha512-4rb8XtXNx7ZaOZarKKnckgz4xLMvds/YrU6mpJfGhGAsy2Mg4mIw1x+DCCGngVGq2soTBVVOxx2s/C6mTX9+pA==} peerDependencies: @@ -3233,6 +4339,11 @@ packages: engines: {node: 20 || >=22} hasBin: true + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3340,6 +4451,9 @@ packages: sift@17.1.3: resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3383,10 +4497,17 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} @@ -3404,9 +4525,15 @@ packages: stable-hash@0.0.4: resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3447,6 +4574,9 @@ packages: string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3531,14 +4661,34 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + 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'} + + to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + to-snake-case@1.0.0: + resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==} + + to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + token-types@6.1.2: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} @@ -3638,6 +4788,21 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} @@ -3670,10 +4835,99 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vite@8.0.5: + resolution: {integrity: sha512-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + 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.1.2: + resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} + 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.1.2 + '@vitest/browser-preview': 4.1.2 + '@vitest/browser-webdriverio': 4.1.2 + '@vitest/ui': 4.1.2 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + 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 + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-worker@1.5.0: resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} @@ -3681,6 +4935,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -3706,6 +4964,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -3758,6 +5021,10 @@ packages: resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} engines: {node: '>=12'} + yjs@13.6.30: + resolution: {integrity: sha512-vv/9h42eCMC81ZHDFswuu/MKzkl/vyq1BhaNGfHyOonwlG4CJbQF4oiBBJPvfdeCt/PlVDWh7Nov9D34YY09uQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3766,6 +5033,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@apidevtools/json-schema-ref-parser@11.7.2': @@ -4308,11 +5578,24 @@ snapshots: react: 19.2.4 tslib: 2.8.1 + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 @@ -4379,79 +5662,233 @@ snapshots: '@epic-web/invariant@1.0.0': {} - '@esbuild/aix-ppc64@0.27.3': + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.13.7 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/linux-x64@0.27.3': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/netbsd-arm64@0.27.3': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/netbsd-x64@0.18.20': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/netbsd-x64@0.27.3': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.3': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/openbsd-x64@0.18.20': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/openbsd-x64@0.27.3': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/openharmony-arm64@0.27.3': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/sunos-x64@0.18.20': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/sunos-x64@0.27.3': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/win32-arm64@0.18.20': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.3': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/win32-ia32@0.18.20': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/win32-ia32@0.27.3': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-x64@0.18.20': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-x64@0.25.12': optional: true '@esbuild/win32-x64@0.27.3': @@ -4631,6 +6068,20 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@floating-ui/react@0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.11 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + tabbable: 6.2.0 + '@floating-ui/react@0.27.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -4761,6 +6212,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -4770,6 +6223,229 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@lexical/clipboard@0.41.0': + dependencies: + '@lexical/html': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/code@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + prismjs: 1.30.0 + + '@lexical/devtools-core@0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@lexical/html': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/mark': 0.41.0 + '@lexical/table': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@lexical/dragon@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + lexical: 0.41.0 + + '@lexical/extension@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + '@preact/signals-core': 1.14.1 + lexical: 0.41.0 + + '@lexical/hashtag@0.41.0': + dependencies: + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/headless@0.41.0': + dependencies: + happy-dom: 20.8.9 + lexical: 0.41.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@lexical/history@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/html@0.41.0': + dependencies: + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/link@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/list@0.41.0': + dependencies: + '@lexical/extension': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/mark@0.41.0': + dependencies: + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/markdown@0.41.0': + dependencies: + '@lexical/code': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/rich-text': 0.41.0 + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/offset@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/overflow@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/plain-text@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/dragon': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/react@0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30)': + dependencies: + '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/devtools-core': 0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/dragon': 0.41.0 + '@lexical/extension': 0.41.0 + '@lexical/hashtag': 0.41.0 + '@lexical/history': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/mark': 0.41.0 + '@lexical/markdown': 0.41.0 + '@lexical/overflow': 0.41.0 + '@lexical/plain-text': 0.41.0 + '@lexical/rich-text': 0.41.0 + '@lexical/table': 0.41.0 + '@lexical/text': 0.41.0 + '@lexical/utils': 0.41.0 + '@lexical/yjs': 0.41.0(yjs@13.6.30) + lexical: 0.41.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-error-boundary: 6.1.1(react@19.2.4) + transitivePeerDependencies: + - yjs + + '@lexical/rich-text@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/dragon': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/selection@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/table@0.41.0': + dependencies: + '@lexical/clipboard': 0.41.0 + '@lexical/extension': 0.41.0 + '@lexical/utils': 0.41.0 + lexical: 0.41.0 + + '@lexical/text@0.41.0': + dependencies: + lexical: 0.41.0 + + '@lexical/utils@0.41.0': + dependencies: + '@lexical/selection': 0.41.0 + lexical: 0.41.0 + + '@lexical/yjs@0.41.0(yjs@13.6.30)': + dependencies: + '@lexical/offset': 0.41.0 + '@lexical/selection': 0.41.0 + lexical: 0.41.0 + yjs: 13.6.30 + + '@libsql/client@0.14.0': + dependencies: + '@libsql/core': 0.14.0 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.8 + libsql: 0.4.7 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.14.0': + dependencies: + js-base64: 3.7.8 + + '@libsql/darwin-arm64@0.4.7': + optional: true + + '@libsql/darwin-x64@0.4.7': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.8 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm64-gnu@0.4.7': + optional: true + + '@libsql/linux-arm64-musl@0.4.7': + optional: true + + '@libsql/linux-x64-gnu@0.4.7': + optional: true + + '@libsql/linux-x64-musl@0.4.7': + optional: true + + '@libsql/win32-x64-msvc@0.4.7': + optional: true + '@monaco-editor/loader@1.5.0': dependencies: state-local: 1.0.7 @@ -4857,6 +6533,15 @@ snapshots: '@napi-rs/nice-win32-x64-msvc': 1.1.1 optional: true + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.7.1 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@neon-rs/load@0.0.4': {} + '@next/env@15.4.11': {} '@next/env@15.5.9': {} @@ -4897,6 +6582,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-project/types@0.122.0': {} + '@payloadcms/db-mongodb@3.81.0(@aws-sdk/credential-providers@3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0)))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(socks@2.8.3)': dependencies: mongoose: 8.15.1(@aws-sdk/credential-providers@3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0)))(socks@2.8.3) @@ -4914,6 +6601,90 @@ snapshots: - socks - supports-color + '@payloadcms/db-sqlite@3.81.0(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))': + dependencies: + '@libsql/client': 0.14.0 + '@payloadcms/drizzle': 3.81.0(@libsql/client@0.14.0)(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3)) + console-table-printer: 2.12.1 + drizzle-kit: 0.31.7 + drizzle-orm: 0.44.7(@libsql/client@0.14.0) + payload: 3.81.0(graphql@16.9.0)(typescript@5.9.3) + prompts: 2.4.2 + to-snake-case: 1.0.0 + uuid: 9.0.0 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bufferutil + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - sql.js + - sqlite3 + - supports-color + - utf-8-validate + + '@payloadcms/drizzle@3.81.0(@libsql/client@0.14.0)(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))': + dependencies: + console-table-printer: 2.12.1 + dequal: 2.0.3 + drizzle-orm: 0.44.7(@libsql/client@0.14.0) + payload: 3.81.0(graphql@16.9.0)(typescript@5.9.3) + prompts: 2.4.2 + to-snake-case: 1.0.0 + uuid: 9.0.0 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - postgres + - prisma + - sql.js + - sqlite3 + '@payloadcms/eslint-config@3.28.0(@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.9.3))(eslint@9.22.0)(typescript@5.9.3))(ts-api-utils@2.5.0(typescript@5.9.3))': dependencies: '@eslint-react/eslint-plugin': 1.31.0(eslint@9.22.0)(ts-api-utils@2.5.0(typescript@5.9.3))(typescript@5.7.3) @@ -5015,6 +6786,52 @@ snapshots: - supports-color - typescript + '@payloadcms/richtext-lexical@3.81.0(@faceless-ui/modal@3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@payloadcms/next@3.81.0(@types/react@19.2.14)(graphql@16.9.0)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(@types/react@19.2.14)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(yjs@13.6.30)': + dependencies: + '@faceless-ui/modal': 3.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@faceless-ui/scroll-info': 2.0.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/clipboard': 0.41.0 + '@lexical/headless': 0.41.0 + '@lexical/html': 0.41.0 + '@lexical/link': 0.41.0 + '@lexical/list': 0.41.0 + '@lexical/mark': 0.41.0 + '@lexical/react': 0.41.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30) + '@lexical/rich-text': 0.41.0 + '@lexical/selection': 0.41.0 + '@lexical/table': 0.41.0 + '@lexical/utils': 0.41.0 + '@payloadcms/next': 3.81.0(@types/react@19.2.14)(graphql@16.9.0)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@payloadcms/translations': 3.81.0 + '@payloadcms/ui': 3.81.0(@types/react@19.2.14)(monaco-editor@0.52.0)(next@15.4.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.77.4))(payload@3.81.0(graphql@16.9.0)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + '@types/uuid': 10.0.0 + acorn: 8.16.0 + bson-objectid: 2.0.4 + csstype: 3.1.3 + dequal: 2.0.3 + escape-html: 1.0.3 + jsox: 1.2.121 + lexical: 0.41.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-jsx: 3.1.3 + micromark-extension-mdx-jsx: 3.0.1 + payload: 3.81.0(graphql@16.9.0)(typescript@5.9.3) + qs-esm: 8.0.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-error-boundary: 4.1.2(react@19.2.4) + ts-essentials: 10.0.3(typescript@5.9.3) + uuid: 10.0.0 + transitivePeerDependencies: + - '@types/react' + - bufferutil + - monaco-editor + - next + - supports-color + - typescript + - utf-8-validate + - yjs + '@payloadcms/translations@3.81.0': dependencies: date-fns: 4.1.0 @@ -5056,6 +6873,60 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@preact/signals-core@1.14.1': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)': + dependencies: + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.12': {} + '@sec-ant/readable-stream@0.4.1': {} '@sindresorhus/is@7.2.0': {} @@ -5380,6 +7251,8 @@ snapshots: tslib: 2.8.1 optional: true + '@standard-schema/spec@1.1.0': {} + '@swc/cli@0.8.1(@swc/core@1.15.24)': dependencies: '@swc/core': 1.15.24 @@ -5470,10 +7343,30 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.8 + '@types/busboy@1.5.4': dependencies: '@types/node': 22.10.1 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} '@types/eslint@9.6.1': @@ -5481,16 +7374,30 @@ snapshots: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/google.maps@3.58.1': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} '@types/json-schema@7.0.15': {} '@types/lodash@4.17.13': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@22.10.1': dependencies: undici-types: 6.20.0 @@ -5509,12 +7416,24 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@types/uuid@10.0.0': {} + '@types/webidl-conversions@7.0.3': {} + '@types/whatwg-mimetype@3.0.2': {} + '@types/whatwg-url@11.0.5': dependencies: '@types/webidl-conversions': 7.0.3 + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.10.1 + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.9.3))(eslint@9.22.0)(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5702,15 +7621,64 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.26.1': + '@typescript-eslint/visitor-keys@8.26.1': + dependencies: + '@typescript-eslint/types': 8.26.1 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.58.0': + dependencies: + '@typescript-eslint/types': 8.58.0 + eslint-visitor-keys: 5.0.1 + + '@vitest/expect@4.1.2': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0) + + '@vitest/mocker@4.1.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 4.1.2 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0) + + '@vitest/pretty-format@4.1.2': dependencies: - '@typescript-eslint/types': 8.26.1 - eslint-visitor-keys: 4.2.1 + tinyrainbow: 3.1.0 - '@typescript-eslint/visitor-keys@8.58.0': + '@vitest/runner@4.1.2': dependencies: - '@typescript-eslint/types': 8.58.0 - eslint-visitor-keys: 5.0.1 + '@vitest/utils': 4.1.2 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + '@vitest/utils': 4.1.2 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.2': {} + + '@vitest/utils@4.1.2': + dependencies: + '@vitest/pretty-format': 4.1.2 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@xhmikosr/archive-type@8.0.1': dependencies: @@ -5884,6 +7852,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -5957,6 +7927,8 @@ snapshots: buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -6001,11 +7973,23 @@ snapshots: caniuse-lite@1.0.30001760: {} + ccount@2.0.1: {} + + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + charenc@0.0.2: {} chokidar@3.6.0: @@ -6062,6 +8046,8 @@ snapshots: convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} + copyfiles@2.4.1: dependencies: glob: 7.2.3 @@ -6099,10 +8085,14 @@ snapshots: cssfilter@0.0.10: {} + csstype@3.1.3: {} + csstype@3.2.3: {} damerau-levenshtein@1.0.8: {} + data-uri-to-buffer@4.0.1: {} + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -6137,6 +8127,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + decompress-response@10.0.0: dependencies: mimic-response: 4.0.0 @@ -6161,8 +8155,13 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.1.2: - optional: true + detect-libc@2.0.2: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 doctrine@3.0.0: dependencies: @@ -6175,6 +8174,19 @@ snapshots: dotenv@17.4.1: {} + drizzle-kit@0.31.7: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + esbuild-register: 3.6.0(esbuild@0.25.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.44.7(@libsql/client@0.14.0): + optionalDependencies: + '@libsql/client': 0.14.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6194,6 +8206,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + entities@7.0.1: {} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 @@ -6259,6 +8273,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6280,6 +8296,67 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-register@3.6.0(esbuild@0.25.12): + dependencies: + debug: 4.4.3 + esbuild: 0.25.12 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.3: optionalDependencies: '@esbuild/aix-ppc64': 0.27.3 @@ -6311,6 +8388,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.1(eslint@9.22.0): @@ -6594,6 +8673,17 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} events-universal@1.0.1: @@ -6629,6 +8719,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.3.0: {} + ext-list@2.2.2: dependencies: mime-db: 1.54.0 @@ -6673,6 +8765,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 @@ -6729,6 +8826,10 @@ snapshots: form-data-encoder@4.1.0: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -6862,6 +8963,18 @@ snapshots: graphql@16.9.0: {} + happy-dom@20.8.9: + dependencies: + '@types/node': 22.10.1 + '@types/whatwg-mimetype': 3.0.2 + '@types/ws': 8.18.1 + entities: 7.0.1 + whatwg-mimetype: 3.0.0 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -6943,6 +9056,13 @@ snapshots: ipaddr.js@2.2.0: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -6991,6 +9111,8 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -7011,6 +9133,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -7081,10 +9205,14 @@ snapshots: isexe@4.0.0: {} + isomorphic.js@0.2.5: {} + jose@5.10.0: {} joycon@3.1.1: {} + js-base64@3.7.8: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -7120,6 +9248,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsox@1.2.121: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -7152,6 +9282,74 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lexical@0.41.0: {} + + lib0@0.2.117: + dependencies: + isomorphic.js: 0.2.5 + + libsql@0.4.7: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.4.7 + '@libsql/darwin-x64': 0.4.7 + '@libsql/linux-arm64-gnu': 0.4.7 + '@libsql/linux-arm64-musl': 0.4.7 + '@libsql/linux-x64-gnu': 0.4.7 + '@libsql/linux-x64-musl': 0.4.7 + '@libsql/win32-x64-msvc': 0.4.7 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} locate-path@6.0.0: @@ -7162,6 +9360,8 @@ snapshots: lodash@4.17.21: {} + longest-streak@3.1.0: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -7170,6 +9370,10 @@ snapshots: lru-cache@11.3.0: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-asynchronous@1.1.0: dependencies: p-event: 6.0.1 @@ -7184,6 +9388,61 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + memoize-one@6.0.0: {} memory-pager@1.5.0: {} @@ -7192,6 +9451,175 @@ snapshots: merge2@1.4.1: {} + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.1: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7268,6 +9696,8 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.11: {} + nanoid@3.3.8: {} natural-compare-lite@1.4.0: {} @@ -7298,6 +9728,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-domexception@1.0.0: {} + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -7305,6 +9737,12 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + noms@0.0.0: dependencies: inherits: 2.0.4 @@ -7361,6 +9799,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} once@1.4.0: @@ -7408,6 +9848,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.26.2 @@ -7436,6 +9886,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + payload@3.81.0(graphql@16.9.0)(typescript@5.9.3): dependencies: '@next/env': 15.5.9 @@ -7534,6 +9986,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier@3.8.1: {} @@ -7542,10 +10000,14 @@ snapshots: dependencies: parse-ms: 4.0.0 + prismjs@1.30.0: {} + process-nextick-args@2.0.1: {} process-warning@5.0.0: {} + promise-limit@2.7.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -7587,6 +10049,15 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-error-boundary@4.1.2(react@19.2.4): + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.4 + + react-error-boundary@6.1.1(react@19.2.4): + dependencies: + react: 19.2.4 + react-image-crop@10.1.8(react@19.2.4): dependencies: react: 19.2.4 @@ -7711,6 +10182,30 @@ snapshots: glob: 13.0.6 package-json-from-dist: 1.0.1 + rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1): + dependencies: + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.12 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1) + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7864,6 +10359,8 @@ snapshots: sift@17.1.3: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-wcswidth@1.0.1: {} @@ -7900,8 +10397,15 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.5.7: {} + source-map@0.6.1: {} + source-map@0.7.6: {} sparse-bitfield@3.0.3: @@ -7915,8 +10419,12 @@ snapshots: stable-hash@0.0.4: {} + stackback@0.0.2: {} + state-local@1.0.7: {} + std-env@4.0.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -7976,6 +10484,11 @@ snapshots: dependencies: safe-buffer: 5.1.2 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8053,15 +10566,31 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tinybench@2.9.0: {} + + tinyexec@1.0.4: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyrainbow@3.1.0: {} + + to-no-case@1.0.2: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + to-snake-case@1.0.0: + dependencies: + to-space-case: 1.0.0 + + to-space-case@1.0.0: + dependencies: + to-no-case: 1.0.2 + token-types@6.1.2: dependencies: '@borewit/text-codec': 0.2.2 @@ -8173,6 +10702,29 @@ snapshots: unicorn-magic@0.3.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + untildify@4.0.0: {} uri-js@4.4.1: @@ -8196,13 +10748,110 @@ snapshots: uuid@10.0.0: {} + uuid@9.0.0: {} + uuid@9.0.1: optional: true + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + esbuild: 0.25.12 + fsevents: 2.3.3 + sass: 1.77.4 + tsx: 4.21.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.8 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1) + tinyglobby: 0.2.15 + optionalDependencies: + esbuild: 0.27.3 + fsevents: 2.3.3 + sass: 1.77.4 + tsx: 4.21.0 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + vitest@4.1.2(happy-dom@20.8.9)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.25.12)(sass@1.77.4)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + happy-dom: 20.8.9 + transitivePeerDependencies: + - msw + + vitest@4.1.2(happy-dom@20.8.9)(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0)): + dependencies: + '@vitest/expect': 4.1.2 + '@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0)) + '@vitest/pretty-format': 4.1.2 + '@vitest/runner': 4.1.2 + '@vitest/snapshot': 4.1.2 + '@vitest/spy': 4.1.2 + '@vitest/utils': 4.1.2 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@emnapi/core@1.9.2)(@emnapi/runtime@1.7.1)(esbuild@0.27.3)(sass@1.77.4)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + happy-dom: 20.8.9 + transitivePeerDependencies: + - msw + + web-streams-polyfill@3.3.3: {} + web-worker@1.5.0: {} webidl-conversions@7.0.0: {} + whatwg-mimetype@3.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 @@ -8253,6 +10902,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -8293,6 +10947,12 @@ snapshots: buffer-crc32: 0.2.13 pend: 1.2.0 + yjs@13.6.30: + dependencies: + lib0: 0.2.117 + yocto-queue@0.1.0: {} yoctocolors@2.1.2: {} + + zwitch@2.0.4: {} diff --git a/geocoding/src/endpoints/geocodingSearch.test.ts b/geocoding/src/endpoints/geocodingSearch.test.ts new file mode 100644 index 00000000..28bd5d5d --- /dev/null +++ b/geocoding/src/endpoints/geocodingSearch.test.ts @@ -0,0 +1,104 @@ +import type { PayloadRequest } from 'payload' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import * as geocodingService from '../services/googleGeocoding.js' +import { createGeocodingSearchEndpoint } from './geocodingSearch.js' + +const MOCK_API_KEY = 'test-api-key' + +const MOCK_RESULT = { + name: 'Berlin', + formattedAddress: 'Berlin, Germany', + googlePlaceId: 'ChIJAVkDPzdOqEcRcDteW0YgIQQ', + location: { lat: 52.52, lng: 13.405 }, + types: ['locality'], +} + +function createMockRequest(overrides: { url: string } & Partial): PayloadRequest { + return { user: { id: '1', email: 'test@test.com' }, ...overrides } as unknown as PayloadRequest +} + +describe('createGeocodingSearchEndpoint', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('returns 401 when user is not authenticated (default access)', async () => { + const endpoint = createGeocodingSearchEndpoint({ apiKey: MOCK_API_KEY }) + const req = createMockRequest({ + url: 'http://localhost/api/geocoding-plugin/search?q=Berlin', + user: null as any, + }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(401) + }) + + it('returns 200 with results for authenticated user', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([MOCK_RESULT]) + + const endpoint = createGeocodingSearchEndpoint({ apiKey: MOCK_API_KEY }) + const req = createMockRequest({ url: 'http://localhost/api/geocoding-plugin/search?q=Berlin' }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(200) + + const body = await response.json() + expect(body.results).toHaveLength(1) + expect(body.results[0].formattedAddress).toBe('Berlin, Germany') + expect(body.results[0].location).toEqual({ lat: 52.52, lng: 13.405 }) + }) + + it('returns 400 when q parameter is missing', async () => { + const endpoint = createGeocodingSearchEndpoint({ apiKey: MOCK_API_KEY }) + const req = createMockRequest({ url: 'http://localhost/api/geocoding-plugin/search' }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(400) + }) + + it('returns 400 when q parameter is empty', async () => { + const endpoint = createGeocodingSearchEndpoint({ apiKey: MOCK_API_KEY }) + const req = createMockRequest({ url: 'http://localhost/api/geocoding-plugin/search?q=%20' }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(400) + }) + + it('uses custom access function when provided', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([MOCK_RESULT]) + + const customAccess = vi.fn().mockResolvedValue(true) + const endpoint = createGeocodingSearchEndpoint({ access: customAccess, apiKey: MOCK_API_KEY }) + const req = createMockRequest({ + url: 'http://localhost/api/geocoding-plugin/search?q=Berlin', + user: null as any, + }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(200) + expect(customAccess).toHaveBeenCalledWith({ req }) + }) + + it('denies access when custom access function returns false', async () => { + const customAccess = vi.fn().mockResolvedValue(false) + const endpoint = createGeocodingSearchEndpoint({ access: customAccess, apiKey: MOCK_API_KEY }) + const req = createMockRequest({ url: 'http://localhost/api/geocoding-plugin/search?q=Berlin' }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(401) + }) + + it('returns 502 when geocoding service fails', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockRejectedValue( + new Error('Google Geocoding API error: REQUEST_DENIED'), + ) + + const endpoint = createGeocodingSearchEndpoint({ apiKey: MOCK_API_KEY }) + const req = createMockRequest({ url: 'http://localhost/api/geocoding-plugin/search?q=Berlin' }) + + const response = await endpoint.handler(req) + expect(response.status).toBe(502) + }) +}) diff --git a/geocoding/src/endpoints/geocodingSearch.ts b/geocoding/src/endpoints/geocodingSearch.ts new file mode 100644 index 00000000..1d726f52 --- /dev/null +++ b/geocoding/src/endpoints/geocodingSearch.ts @@ -0,0 +1,45 @@ +import type { Endpoint, PayloadRequest } from 'payload' + +import type { GeocodingEndpointAccess } from '../types/GeoCodingPluginConfig.js' + +import { geocodeAddress } from '../services/googleGeocoding.js' + +/** + * Creates a Payload endpoint for server-side geocoding. + * Enables agents and API consumers to geocode addresses without the browser UI. + * + * Usage: GET /api/geocoding-plugin/search?q=Berlin + */ +export const createGeocodingSearchEndpoint = (options: { + access?: GeocodingEndpointAccess + apiKey: string +}): Endpoint => ({ + handler: async (req: PayloadRequest) => { + // Authentication: require a logged-in user by default + const hasAccess = options.access ? await options.access({ req }) : Boolean(req.user) + + if (!hasAccess) { + return Response.json({ errors: [{ message: 'Unauthorized' }] }, { status: 401 }) + } + + const url = new URL(req.url!) + const query = url.searchParams.get('q') + + if (!query || query.trim() === '') { + return Response.json( + { errors: [{ message: 'Query parameter "q" is required' }] }, + { status: 400 }, + ) + } + + try { + const results = await geocodeAddress(query, options.apiKey) + return Response.json({ results }) + } catch (error) { + const message = error instanceof Error ? error.message : 'Geocoding failed' + return Response.json({ errors: [{ message }] }, { status: 502 }) + } + }, + method: 'get', + path: '/geocoding-plugin/search', +}) diff --git a/geocoding/src/fields/geocodingField.test.ts b/geocoding/src/fields/geocodingField.test.ts new file mode 100644 index 00000000..62c7ba9b --- /dev/null +++ b/geocoding/src/fields/geocodingField.test.ts @@ -0,0 +1,59 @@ +import type { JSONField, PointField, RowField, TextField } from 'payload' + +import { describe, expect, it } from 'vitest' + +import { geocodingField } from './geocodingField.js' + +describe('geocodingField', () => { + it('creates a row field with meta, point, and address fields', () => { + const field = geocodingField({ + pointField: { name: 'location', type: 'point' }, + }) as RowField + + expect(field.type).toBe('row') + expect(field.fields).toHaveLength(3) + }) + + it('adds a hidden virtual address text field for server-side geocoding', () => { + const field = geocodingField({ + pointField: { name: 'location', type: 'point' }, + }) as RowField + + const addressField = field.fields.find( + (f) => 'name' in f && f.name === 'location_address', + ) as TextField + expect(addressField).toBeDefined() + expect(addressField.type).toBe('text') + expect(addressField.admin?.hidden).toBe(true) + expect(addressField.virtual).toBe(true) + }) + + it('adds beforeChange hooks to both meta and point fields', () => { + const field = geocodingField({ + pointField: { name: 'location', type: 'point' }, + }) as RowField + + const metaField = field.fields.find( + (f) => 'name' in f && f.name === 'location_meta', + ) as JSONField + expect(metaField.hooks?.beforeChange).toHaveLength(1) + + const pointField = field.fields.find((f) => 'name' in f && f.name === 'location') as PointField + expect(pointField.hooks?.beforeChange).toHaveLength(1) + }) + + it('preserves existing point field hooks', () => { + const existingHook = () => undefined + const field = geocodingField({ + pointField: { + name: 'location', + type: 'point', + hooks: { beforeChange: [existingHook] }, + }, + }) as RowField + + const pointField = field.fields.find((f) => 'name' in f && f.name === 'location') as PointField + expect(pointField.hooks?.beforeChange).toHaveLength(2) + expect(pointField.hooks!.beforeChange![0]).toBe(existingHook) + }) +}) diff --git a/geocoding/src/fields/geocodingField.ts b/geocoding/src/fields/geocodingField.ts index 046ec233..f702ba09 100644 --- a/geocoding/src/fields/geocodingField.ts +++ b/geocoding/src/fields/geocodingField.ts @@ -1,53 +1,87 @@ -import type { Field } from 'payload' +import type { Field, PointField } from 'payload' import type { GeoCodingFieldConfig } from '../types/GeoCodingFieldConfig.js' +import { + createMetaBeforeChangeHook, + createPointBeforeChangeHook, +} from '../hooks/geocodeBeforeChange.js' + /** * Creates a row field containing: * 1. The provided point field for storing the coordinates from the Google Places API * 2. A JSON field that stores location metadata (display name, address, googlePlaceId) + * 3. A hidden virtual text field `{pointFieldName}_address` for server-side geocoding via the API + * + * Agents and API consumers can submit an address string via the `_address` field, + * and the beforeChange hooks will auto-geocode it and populate the point and meta fields. */ export const geocodingField = (config: GeoCodingFieldConfig): Field => { + const pointFieldName = config.pointField.name + + const metaField: Field = { + name: pointFieldName + '_meta', + type: 'json', + access: config.locationMetaOverride?.access ?? {}, + admin: { + // overridable props: + readOnly: true, + + ...config.locationMetaOverride?.admin, + + // non-overridable props: + components: { + Field: '@jhb.software/payload-geocoding-plugin/server#GeocodingField', + }, + }, + hooks: { + beforeChange: [createMetaBeforeChangeHook({ pointFieldName })], + }, + jsonSchema: { + fileMatch: ['a://b/foo.json'], + schema: { + type: 'object', + additionalProperties: false, + properties: { + name: { type: 'string' }, + formattedAddress: { type: 'string' }, + googlePlaceId: { type: 'string' }, + types: { type: 'array', items: { type: 'string' } }, + }, + required: ['formattedAddress', 'googlePlaceId', 'name', 'types'], + }, + uri: 'a://b/foo.json', + }, + label: config.locationMetaOverride?.label ?? 'Location', + required: config.locationMetaOverride?.required, + } + + const pointField: PointField = { + ...config.pointField, + hooks: { + ...config.pointField.hooks, + beforeChange: [ + ...(config.pointField.hooks?.beforeChange ?? []), + createPointBeforeChangeHook({ pointFieldName }), + ], + }, + } + + const addressField: Field = { + name: pointFieldName + '_address', + type: 'text', + admin: { + hidden: true, + }, + label: 'Address (for server-side geocoding)', + virtual: true, + } + return { type: 'row', admin: { position: config.pointField.admin?.position ?? undefined, }, - fields: [ - { - name: config.pointField.name + '_meta', - type: 'json', - access: config.locationMetaOverride?.access ?? {}, - admin: { - // overridable props: - readOnly: true, - - ...config.locationMetaOverride?.admin, - - // non-overridable props: - components: { - Field: '@jhb.software/payload-geocoding-plugin/server#GeocodingField', - }, - }, - jsonSchema: { - fileMatch: ['a://b/foo.json'], - schema: { - type: 'object', - additionalProperties: false, - properties: { - name: { type: 'string' }, - formattedAddress: { type: 'string' }, - googlePlaceId: { type: 'string' }, - types: { type: 'array', items: { type: 'string' } }, - }, - required: ['formattedAddress', 'googlePlaceId', 'name', 'types'], - }, - uri: 'a://b/foo.json', - }, - label: config.locationMetaOverride?.label ?? 'Location', - required: config.locationMetaOverride?.required, - }, - config.pointField, - ], + fields: [metaField, pointField, addressField], } } diff --git a/geocoding/src/hooks/geocodeBeforeChange.test.ts b/geocoding/src/hooks/geocodeBeforeChange.test.ts new file mode 100644 index 00000000..44f25a29 --- /dev/null +++ b/geocoding/src/hooks/geocodeBeforeChange.test.ts @@ -0,0 +1,150 @@ +import type { FieldHookArgs } from 'payload' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import * as geocodingService from '../services/googleGeocoding.js' +import { createMetaBeforeChangeHook, createPointBeforeChangeHook } from './geocodeBeforeChange.js' + +const MOCK_API_KEY = 'test-api-key' + +const MOCK_RESULT = { + name: 'Alexanderplatz', + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + googlePlaceId: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + location: { lat: 52.5219, lng: 13.4132 }, + types: ['point_of_interest'], +} + +function createMockReq() { + return { + payload: { + config: { + custom: { + payloadGeocodingPlugin: { + googleMapsApiKey: MOCK_API_KEY, + }, + }, + }, + }, + } +} + +function createMockHookArgs(overrides: Partial): FieldHookArgs { + return { + blockData: undefined, + collection: null, + context: {}, + data: {}, + field: {} as any, + operation: 'create', + originalDoc: undefined, + path: [] as any, + req: createMockReq() as any, + schemaPath: [] as any, + siblingData: {}, + value: undefined, + ...overrides, + } as FieldHookArgs +} + +describe('createMetaBeforeChangeHook', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('geocodes an address string and returns location metadata', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([MOCK_RESULT]) + + const hook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + + const result = await hook( + createMockHookArgs({ + siblingData: { location_address: 'Alexanderplatz, Berlin' }, + }), + ) + + expect(result).toEqual({ + name: 'Alexanderplatz', + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + googlePlaceId: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + types: ['point_of_interest'], + }) + }) + + it('returns undefined when no address is provided', async () => { + const hook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + const result = await hook(createMockHookArgs({ siblingData: {} })) + expect(result).toBeUndefined() + }) + + it('returns undefined when address is empty string', async () => { + const hook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + const result = await hook(createMockHookArgs({ siblingData: { location_address: ' ' } })) + expect(result).toBeUndefined() + }) + + it('returns undefined when geocoding returns no results', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([]) + const hook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + const result = await hook( + createMockHookArgs({ siblingData: { location_address: 'xyznonexistent' } }), + ) + expect(result).toBeUndefined() + }) + + it('throws when API key is not configured', async () => { + const hook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + await expect( + hook( + createMockHookArgs({ + req: { payload: { config: { custom: {} } } } as any, + siblingData: { location_address: 'Berlin' }, + }), + ), + ).rejects.toThrow('Geocoding plugin API key not configured') + }) +}) + +describe('createPointBeforeChangeHook', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('returns [lng, lat] from geocoded address', async () => { + vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([MOCK_RESULT]) + + const hook = createPointBeforeChangeHook({ pointFieldName: 'location' }) + const result = await hook( + createMockHookArgs({ + siblingData: { location_address: 'Berlin' }, + }), + ) + + expect(result).toEqual([13.4132, 52.5219]) + }) + + it('returns existing value when no address is provided', async () => { + const hook = createPointBeforeChangeHook({ pointFieldName: 'location' }) + const result = await hook(createMockHookArgs({ siblingData: {}, value: [1, 2] })) + expect(result).toEqual([1, 2]) + }) + + it('shares cached geocoding result via context', async () => { + const geocodeSpy = vi.spyOn(geocodingService, 'geocodeAddress').mockResolvedValue([MOCK_RESULT]) + + const context: Record = {} + const hookArgs = { context, siblingData: { location_address: 'Berlin' } } + + const metaHook = createMetaBeforeChangeHook({ pointFieldName: 'location' }) + const pointHook = createPointBeforeChangeHook({ pointFieldName: 'location' }) + + const [metaResult, pointResult] = await Promise.all([ + metaHook(createMockHookArgs(hookArgs)), + pointHook(createMockHookArgs(hookArgs)), + ]) + + expect(metaResult).toHaveProperty('formattedAddress') + expect(pointResult).toEqual([13.4132, 52.5219]) + expect(geocodeSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/geocoding/src/hooks/geocodeBeforeChange.ts b/geocoding/src/hooks/geocodeBeforeChange.ts new file mode 100644 index 00000000..c90e243b --- /dev/null +++ b/geocoding/src/hooks/geocodeBeforeChange.ts @@ -0,0 +1,112 @@ +import type { FieldHook } from 'payload' + +import type { GeocodingResult } from '../services/googleGeocoding.js' + +import { geocodeAddress } from '../services/googleGeocoding.js' + +/** + * Gets (or triggers) the geocoding result for the given address, caching it in + * `req.context` so that the meta and point field hooks share a single API call. + * + * Both hooks may fire concurrently (Payload processes row fields in parallel), + * so the promise itself is cached — whichever hook runs first starts the fetch, + * and the other awaits the same promise. + */ +function getCachedGeocodingResult( + cacheKey: string, + address: string, + apiKey: string, + context: Record, +): Promise { + if (!context[cacheKey]) { + context[cacheKey] = geocodeAddress(address.trim(), apiKey) + } + return context[cacheKey] as Promise +} + +function getApiKey(req: { payload: { config: { custom?: Record } } }): string { + const apiKey = ( + req.payload.config.custom?.payloadGeocodingPlugin as { googleMapsApiKey?: string } | undefined + )?.googleMapsApiKey + + if (!apiKey) { + throw new Error( + 'Geocoding plugin API key not configured. Ensure payloadGeocodingPlugin is added to your Payload config with a googleMapsApiKey.', + ) + } + + return apiKey +} + +function getAddress( + addressFieldName: string, + siblingData?: Record, + data?: Record, +): string | undefined { + const address = siblingData?.[addressFieldName] ?? data?.[addressFieldName] + if (!address || typeof address !== 'string' || address.trim() === '') { + return undefined + } + return address +} + +/** + * beforeChange hook for the **location metadata JSON field**. + * + * When `{pointFieldName}_address` contains a string, geocodes it server-side + * and returns the location metadata as the field value. + */ +export const createMetaBeforeChangeHook = (options: { pointFieldName: string }): FieldHook => { + return async ({ context, data, req, siblingData }) => { + const addressFieldName = options.pointFieldName + '_address' + const address = getAddress(addressFieldName, siblingData, data) + + if (!address) { + return undefined + } + + const apiKey = getApiKey(req) + const cacheKey = `geocoding_${options.pointFieldName}` + const results = await getCachedGeocodingResult(cacheKey, address, apiKey, context) + + if (results.length === 0) { + return undefined + } + + const firstResult = results[0] + return { + name: firstResult.name, + formattedAddress: firstResult.formattedAddress, + googlePlaceId: firstResult.googlePlaceId, + types: firstResult.types, + } + } +} + +/** + * beforeChange hook for the **point field**. + * + * When `{pointFieldName}_address` contains a string, geocodes it server-side + * and returns `[lng, lat]` as the field value. + */ +export const createPointBeforeChangeHook = (options: { pointFieldName: string }): FieldHook => { + return async ({ context, data, req, siblingData, value }) => { + const addressFieldName = options.pointFieldName + '_address' + const address = getAddress(addressFieldName, siblingData, data) + + if (!address) { + return value + } + + const apiKey = getApiKey(req) + const cacheKey = `geocoding_${options.pointFieldName}` + const results = await getCachedGeocodingResult(cacheKey, address, apiKey, context) + + if (results.length === 0) { + return value + } + + const firstResult = results[0] + return [firstResult.location.lng, firstResult.location.lat] + } +} diff --git a/geocoding/src/index.ts b/geocoding/src/index.ts index 53c84cd5..490dfd80 100644 --- a/geocoding/src/index.ts +++ b/geocoding/src/index.ts @@ -1,4 +1,9 @@ export { geocodingField } from './fields/geocodingField.js' export { payloadGeocodingPlugin } from './plugin.js' +export { geocodeAddress } from './services/googleGeocoding.js' +export type { GeocodingResult } from './services/googleGeocoding.js' export type { GeoCodingFieldConfig } from './types/GeoCodingFieldConfig.js' -export type { GeocodingPluginConfig } from './types/GeoCodingPluginConfig.js' +export type { + GeocodingEndpointAccess, + GeocodingPluginConfig, +} from './types/GeoCodingPluginConfig.js' diff --git a/geocoding/src/plugin.test.ts b/geocoding/src/plugin.test.ts new file mode 100644 index 00000000..6604157f --- /dev/null +++ b/geocoding/src/plugin.test.ts @@ -0,0 +1,60 @@ +import type { Config } from 'payload' + +import { describe, expect, it } from 'vitest' + +import { payloadGeocodingPlugin } from './plugin.js' + +const BASE_CONFIG: Config = { + collections: [], + db: {} as any, + secret: 'test', +} + +describe('payloadGeocodingPlugin', () => { + it('registers the geocoding search endpoint when enabled', () => { + const plugin = payloadGeocodingPlugin({ + googleMapsApiKey: 'test-key', + }) + const config = plugin(BASE_CONFIG) + + expect(config.endpoints).toBeDefined() + expect(config.endpoints).toHaveLength(1) + expect(config.endpoints![0]).toMatchObject({ + method: 'get', + path: '/geocoding-plugin/search', + }) + }) + + it('does not register endpoint when plugin is disabled', () => { + const plugin = payloadGeocodingPlugin({ + enabled: false, + googleMapsApiKey: 'test-key', + }) + const config = plugin(BASE_CONFIG) + + expect(config.endpoints ?? []).toHaveLength(0) + }) + + it('stores the API key in config.custom', () => { + const plugin = payloadGeocodingPlugin({ + googleMapsApiKey: 'my-api-key', + }) + const config = plugin(BASE_CONFIG) + + expect(config.custom?.payloadGeocodingPlugin?.googleMapsApiKey).toBe('my-api-key') + }) + + it('preserves existing endpoints from incoming config', () => { + const existingEndpoint = { + handler: () => Response.json({ ok: true }), + method: 'get' as const, + path: '/health', + } + const plugin = payloadGeocodingPlugin({ + googleMapsApiKey: 'test-key', + }) + const config = plugin({ ...BASE_CONFIG, endpoints: [existingEndpoint] }) + + expect(config.endpoints).toHaveLength(2) + }) +}) diff --git a/geocoding/src/plugin.ts b/geocoding/src/plugin.ts index cfc93e1f..09cf9b94 100644 --- a/geocoding/src/plugin.ts +++ b/geocoding/src/plugin.ts @@ -2,6 +2,8 @@ import type { Config } from 'payload' import type { GeocodingPluginConfig } from './types/GeoCodingPluginConfig.js' +import { createGeocodingSearchEndpoint } from './endpoints/geocodingSearch.js' + /** * Payload plugin which extends the point field with geocoding functionality. */ @@ -13,6 +15,11 @@ export const payloadGeocodingPlugin = return incomingConfig } + const geocodingEndpoint = createGeocodingSearchEndpoint({ + access: pluginOptions.geocodingEndpoint?.access, + apiKey: pluginOptions.googleMapsApiKey, + }) + // Store API key in config.custom for server component access const config: Config = { ...incomingConfig, @@ -22,6 +29,7 @@ export const payloadGeocodingPlugin = googleMapsApiKey: pluginOptions.googleMapsApiKey, }, }, + endpoints: [...(incomingConfig.endpoints ?? []), geocodingEndpoint], } return config diff --git a/geocoding/src/services/googleGeocoding.test.ts b/geocoding/src/services/googleGeocoding.test.ts new file mode 100644 index 00000000..970d1014 --- /dev/null +++ b/geocoding/src/services/googleGeocoding.test.ts @@ -0,0 +1,82 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { geocodeAddress } from './googleGeocoding.js' + +const MOCK_API_KEY = 'test-api-key' + +const MOCK_GOOGLE_RESPONSE = { + results: [ + { + address_components: [ + { long_name: 'Alexanderplatz', short_name: 'Alexanderplatz', types: ['point_of_interest'] }, + { long_name: 'Berlin', short_name: 'Berlin', types: ['locality'] }, + ], + formatted_address: 'Alexanderplatz, 10178 Berlin, Germany', + geometry: { + location: { lat: 52.5219, lng: 13.4132 }, + }, + place_id: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + types: ['point_of_interest', 'establishment'], + }, + ], + status: 'OK', +} + +describe('geocodeAddress', () => { + beforeEach(() => { + vi.restoreAllMocks() + }) + + it('returns geocoded results for a valid address', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify(MOCK_GOOGLE_RESPONSE), { status: 200 }), + ) + + const results = await geocodeAddress('Alexanderplatz, Berlin', MOCK_API_KEY) + + expect(results).toHaveLength(1) + expect(results[0]).toEqual({ + name: 'Alexanderplatz', + formattedAddress: 'Alexanderplatz, 10178 Berlin, Germany', + googlePlaceId: 'ChIJp1l4uWBRqEcR2SPNRBMhtAI', + location: { lat: 52.5219, lng: 13.4132 }, + types: ['point_of_interest', 'establishment'], + }) + + const fetchCall = vi.mocked(fetch).mock.calls[0][0] as string + expect(fetchCall).toContain('address=Alexanderplatz') + expect(fetchCall).toContain(`key=${MOCK_API_KEY}`) + }) + + it('returns empty array for ZERO_RESULTS', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify({ results: [], status: 'ZERO_RESULTS' }), { status: 200 }), + ) + + const results = await geocodeAddress('xyznonexistent12345', MOCK_API_KEY) + expect(results).toEqual([]) + }) + + it('throws on API error status', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response( + JSON.stringify({ error_message: 'Invalid key', results: [], status: 'REQUEST_DENIED' }), + { status: 200 }, + ), + ) + + await expect(geocodeAddress('Berlin', MOCK_API_KEY)).rejects.toThrow( + 'Google Geocoding API error: REQUEST_DENIED - Invalid key', + ) + }) + + it('throws on HTTP failure', async () => { + vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }), + ) + + await expect(geocodeAddress('Berlin', MOCK_API_KEY)).rejects.toThrow( + 'Google Geocoding API request failed: 500 Internal Server Error', + ) + }) +}) diff --git a/geocoding/src/services/googleGeocoding.ts b/geocoding/src/services/googleGeocoding.ts new file mode 100644 index 00000000..4d20b486 --- /dev/null +++ b/geocoding/src/services/googleGeocoding.ts @@ -0,0 +1,69 @@ +export type GeocodingResult = { + formattedAddress: string + googlePlaceId: string + location: { + lat: number + lng: number + } + name: string + types: string[] +} + +type GoogleGeocodingApiResponse = { + error_message?: string + results: Array<{ + address_components: Array<{ + long_name: string + short_name: string + types: string[] + }> + formatted_address: string + geometry: { + location: { + lat: number + lng: number + } + } + place_id: string + types: string[] + }> + status: string +} + +/** + * Server-side geocoding using the Google Geocoding HTTP API. + * This enables agents and API consumers to geocode addresses without the browser-based Places UI. + */ +export async function geocodeAddress(address: string, apiKey: string): Promise { + const url = new URL('https://maps.googleapis.com/maps/api/geocode/json') + url.searchParams.set('address', address) + url.searchParams.set('key', apiKey) + + const response = await fetch(url.toString()) + + if (!response.ok) { + throw new Error( + `Google Geocoding API request failed: ${response.status} ${response.statusText}`, + ) + } + + const data: GoogleGeocodingApiResponse = await response.json() + + if (data.status === 'ZERO_RESULTS') { + return [] + } + + if (data.status !== 'OK') { + throw new Error( + `Google Geocoding API error: ${data.status} - ${data.error_message ?? 'Unknown error'}`, + ) + } + + return data.results.map((result) => ({ + name: result.address_components[0]?.long_name ?? result.formatted_address, + formattedAddress: result.formatted_address, + googlePlaceId: result.place_id, + location: result.geometry.location, + types: result.types, + })) +} diff --git a/geocoding/src/types/GeoCodingPluginConfig.ts b/geocoding/src/types/GeoCodingPluginConfig.ts index f778e8f8..8beb08f1 100644 --- a/geocoding/src/types/GeoCodingPluginConfig.ts +++ b/geocoding/src/types/GeoCodingPluginConfig.ts @@ -1,7 +1,21 @@ +import type { PayloadRequest } from 'payload' + +/** Access function for the geocoding endpoint. */ +export type GeocodingEndpointAccess = (args: { req: PayloadRequest }) => boolean | Promise + /** Configuration options for the geocoding plugin. */ export type GeocodingPluginConfig = { /** Whether the geocoding plugin is enabled. */ enabled?: boolean + /** Configuration for the server-side geocoding search endpoint (GET /api/geocoding-plugin/search). */ + geocodingEndpoint?: { + /** + * Custom access function to control who can use the geocoding endpoint. + * Receives the Payload request object. Return true to allow access. + * Defaults to requiring an authenticated user (i.e. `req.user` must be truthy). + */ + access?: GeocodingEndpointAccess + } /** Google Maps API key for geocoding functionality. */ googleMapsApiKey: string } From f3b70da15112aa27e704c2f61cbdf5a5bf89db24 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Apr 2026 08:00:14 +0000 Subject: [PATCH 14/14] chore(geocoding): update generated payload types for articles collection https://claude.ai/code/session_01Ja3JVC48ozET22Z7yJkyY6 --- geocoding/dev/src/payload-types.ts | 77 +++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/geocoding/dev/src/payload-types.ts b/geocoding/dev/src/payload-types.ts index 9b488a54..4914ac89 100644 --- a/geocoding/dev/src/payload-types.ts +++ b/geocoding/dev/src/payload-types.ts @@ -69,6 +69,7 @@ export interface Config { collections: { users: User; pages: Page; + articles: Article; 'payload-kv': PayloadKv; 'payload-locked-documents': PayloadLockedDocument; 'payload-preferences': PayloadPreference; @@ -78,13 +79,14 @@ export interface Config { collectionsSelect: { users: UsersSelect | UsersSelect; pages: PagesSelect | PagesSelect; + articles: ArticlesSelect | ArticlesSelect; 'payload-kv': PayloadKvSelect | PayloadKvSelect; 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; }; db: { - defaultIDType: string; + defaultIDType: number; }; fallbackLocale: null; globals: {}; @@ -122,7 +124,7 @@ export interface UserAuthOperations { * via the `definition` "users". */ export interface User { - id: string; + id: number; updatedAt: string; createdAt: string; email: string; @@ -147,7 +149,7 @@ export interface User { * via the `definition` "pages". */ export interface Page { - id: string; + id: number; title?: string | null; location_meta?: { name: string; @@ -160,6 +162,7 @@ export interface Page { * @maxItems 2 */ location?: [number, number] | null; + location_address?: string | null; location0_meta?: { name: string; formattedAddress: string; @@ -171,6 +174,7 @@ export interface Page { * @maxItems 2 */ location0?: [number, number] | null; + location0_address?: string | null; location1_meta?: { name: string; formattedAddress: string; @@ -182,6 +186,7 @@ export interface Page { * @maxItems 2 */ location1: [number, number]; + location1_address?: string | null; location2_meta: { name: string; formattedAddress: string; @@ -193,6 +198,7 @@ export interface Page { * @maxItems 2 */ location2: [number, number]; + location2_address?: string | null; location3_meta?: { name: string; formattedAddress: string; @@ -204,6 +210,7 @@ export interface Page { * @maxItems 2 */ location3?: [number, number] | null; + location3_address?: string | null; locationGroup?: { location_meta?: { name: string; @@ -216,6 +223,7 @@ export interface Page { * @maxItems 2 */ location?: [number, number] | null; + location_address?: string | null; }; locations?: | { @@ -230,18 +238,44 @@ export interface Page { * @maxItems 2 */ location?: [number, number] | null; + location_address?: string | null; id?: string | null; }[] | null; updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "articles". + */ +export interface Article { + id: number; + title: string; + content?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv". */ export interface PayloadKv { - id: string; + id: number; key: string; data: | { @@ -258,20 +292,24 @@ export interface PayloadKv { * via the `definition` "payload-locked-documents". */ export interface PayloadLockedDocument { - id: string; + id: number; document?: | ({ relationTo: 'users'; - value: string | User; + value: number | User; } | null) | ({ relationTo: 'pages'; - value: string | Page; + value: number | Page; + } | null) + | ({ + relationTo: 'articles'; + value: number | Article; } | null); globalSlug?: string | null; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; updatedAt: string; createdAt: string; @@ -281,10 +319,10 @@ export interface PayloadLockedDocument { * via the `definition` "payload-preferences". */ export interface PayloadPreference { - id: string; + id: number; user: { relationTo: 'users'; - value: string | User; + value: number | User; }; key?: string | null; value?: @@ -304,7 +342,7 @@ export interface PayloadPreference { * via the `definition` "payload-migrations". */ export interface PayloadMigration { - id: string; + id: number; name?: string | null; batch?: number | null; updatedAt: string; @@ -340,30 +378,47 @@ export interface PagesSelect { title?: T; location_meta?: T; location?: T; + location_address?: T; location0_meta?: T; location0?: T; + location0_address?: T; location1_meta?: T; location1?: T; + location1_address?: T; location2_meta?: T; location2?: T; + location2_address?: T; location3_meta?: T; location3?: T; + location3_address?: T; locationGroup?: | T | { location_meta?: T; location?: T; + location_address?: T; }; locations?: | T | { location_meta?: T; location?: T; + location_address?: T; id?: T; }; updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "articles_select". + */ +export interface ArticlesSelect { + title?: T; + content?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "payload-kv_select".