@chaosity/location-client-react 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,7 +116,7 @@ function MapComponent() {
116
116
  Provides the location client and automatic token refresh to all child components.
117
117
 
118
118
  ```tsx
119
- <LocationClientProvider getConfig={getLocationConfig} refreshBuffer={60}>
119
+ <LocationClientProvider getConfig={getLocationConfig}>
120
120
  {children}
121
121
  </LocationClientProvider>
122
122
  ```
@@ -124,9 +124,14 @@ Provides the location client and automatic token refresh to all child components
124
124
  **Props:**
125
125
 
126
126
  - `getConfig` — Async function that returns `{ apiUrl: string, token: string, expiresAt?: number }`. Called on init and whenever the token needs refreshing.
127
- - `refreshBuffer` (optional, default: `60`) — Seconds before token expiry to proactively refresh. Prevents mid-request expiration.
128
127
  - `children` — Child components.
129
128
 
129
+ There is no `refreshBuffer` prop. It was removed in `0.3.0` — a value shorter
130
+ than the server's own re-mint window made the client judge a token stale that
131
+ the server would not yet replace, and the two spun against each other. Both
132
+ sides now apply the same buffer to the token's own `exp`. Passing it does
133
+ nothing.
134
+
130
135
  ### useLocationClient
131
136
 
132
137
  Hook to access the location client in any component.
@@ -137,7 +142,7 @@ const { client, getToken, loading, error } = useLocationClient()
137
142
 
138
143
  **Returns:**
139
144
 
140
- - `client` (`GeoPlacesClient | null`) — The location client instance. Automatically refreshes the token before each `send()` call if needed.
145
+ - `client` (`LocationClient | null`) — The location client. Not a bare `GeoPlacesClient`: the provider wraps it so `send()` refreshes the token first when it needs to, and retries once if the API rejects it.
141
146
  - `getToken` (`() => string | undefined`) — Returns the current token. Useful for direct API calls (e.g., map style fetch).
142
147
  - `loading` (`boolean`) — Whether the client is initializing.
143
148
  - `error` (`string | null`) — Error message if initialization or token refresh failed.
@@ -146,14 +151,30 @@ const { client, getToken, loading, error } = useLocationClient()
146
151
 
147
152
  ## Token Refresh
148
153
 
149
- The provider automatically handles token lifecycle:
150
-
151
- 1. Fetches an initial token via `getConfig` on mount
152
- 2. Before each `client.send()` call, checks if the token is expired or within the `refreshBuffer` window
153
- 3. If expired, calls `getConfig` again to get a fresh token
154
- 4. Concurrent refresh requests are deduplicated — multiple `send()` calls wait for the same refresh
155
-
156
- No manual token management needed. The `client` always uses a valid token.
154
+ The provider owns the token lifecycle. There is nothing to manage manually.
155
+
156
+ 1. `getConfig` is called on mount for the initial token.
157
+ 2. A timer refreshes **ahead of expiry**, 60 seconds before the token's own
158
+ `exp`. This is what keeps a map alive: MapLibre requests tiles, glyphs and
159
+ sprites directly, never through `send()`, so a refresh that happened only
160
+ inside `send()` would never fire for them.
161
+ 3. `send()` checks too, and refreshes first if the token is inside that window.
162
+ 4. Returning to a backgrounded tab refreshes immediately — a throttled tab's
163
+ timer can be arbitrarily late.
164
+ 5. If the API rejects a token **before** its `exp` — revoked from the portal, or
165
+ minted against a client secret since rotated — the 401 triggers a refresh and
166
+ the request is retried once with the new token. Nothing on this side has any
167
+ other reason to replace that token, so without this the failures continue
168
+ until the timer next comes around: for a token with 14 minutes left, 14
169
+ minutes of a broken page. Needs `@chaosity/location-client` 0.7.0 or later;
170
+ on older versions the other five steps still work.
171
+ 6. Concurrent refreshes are deduplicated — everything waiting shares one call to
172
+ `getConfig`.
173
+
174
+ A refresh that fails is reported as `error` from `useLocationClient()`, and
175
+ rejects the `send()` that triggered it — with the refresh error rather than a
176
+ 401, so the cause reads as the token endpoint being unreachable and not as the
177
+ API refusing you.
157
178
 
158
179
  ## Complete Example with MapLibre
159
180
 
@@ -0,0 +1,16 @@
1
+ import type { MapLike } from '@chaosity/location-client';
2
+ /**
3
+ * React hook that keeps map label language in sync with the `language` prop.
4
+ *
5
+ * Registers a persistent `style.load` listener so language is automatically
6
+ * reapplied whenever `map.setStyle()` is called (e.g. style or color scheme change).
7
+ * Also applies immediately if the style is already loaded.
8
+ *
9
+ * @param map - MapLibre Map instance, or null while the map is initializing
10
+ * @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja')
11
+ *
12
+ * @example
13
+ * const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
14
+ * useMapLanguage(mapInstance, language)
15
+ */
16
+ export declare function useMapLanguage(map: MapLike | null, language: string): void;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.useMapLanguage = useMapLanguage;
5
+ const location_client_1 = require("@chaosity/location-client");
6
+ const react_1 = require("react");
7
+ /**
8
+ * React hook that keeps map label language in sync with the `language` prop.
9
+ *
10
+ * Registers a persistent `style.load` listener so language is automatically
11
+ * reapplied whenever `map.setStyle()` is called (e.g. style or color scheme change).
12
+ * Also applies immediately if the style is already loaded.
13
+ *
14
+ * @param map - MapLibre Map instance, or null while the map is initializing
15
+ * @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja')
16
+ *
17
+ * @example
18
+ * const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
19
+ * useMapLanguage(mapInstance, language)
20
+ */
21
+ function useMapLanguage(map, language) {
22
+ (0, react_1.useEffect)(() => {
23
+ if (!map)
24
+ return;
25
+ const applyFn = () => (0, location_client_1.applyMapLanguage)(map, language);
26
+ if (map.isStyleLoaded()) {
27
+ applyFn();
28
+ }
29
+ map.on('style.load', applyFn);
30
+ return () => {
31
+ map.off('style.load', applyFn);
32
+ };
33
+ }, [map, language]);
34
+ }
@@ -0,0 +1,3 @@
1
+ export { LocationClientProvider, useLocationClient, } from './provider/LocationClientProvider.js';
2
+ export type { LocationClient, LocationClientProviderProps, SendOptions, } from './provider/LocationClientProvider.js';
3
+ export { useMapLanguage } from './hooks/useMapLanguage.js';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useMapLanguage = exports.useLocationClient = exports.LocationClientProvider = void 0;
4
+ var LocationClientProvider_js_1 = require("./provider/LocationClientProvider.js");
5
+ Object.defineProperty(exports, "LocationClientProvider", { enumerable: true, get: function () { return LocationClientProvider_js_1.LocationClientProvider; } });
6
+ Object.defineProperty(exports, "useLocationClient", { enumerable: true, get: function () { return LocationClientProvider_js_1.useLocationClient; } });
7
+ var useMapLanguage_js_1 = require("./hooks/useMapLanguage.js");
8
+ Object.defineProperty(exports, "useMapLanguage", { enumerable: true, get: function () { return useMapLanguage_js_1.useMapLanguage; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,63 @@
1
+ import type { ClientConfig } from '@chaosity/location-client';
2
+ import { type AppConfigClaims } from '@chaosity/location-client';
3
+ import type { ReactNode } from 'react';
4
+ /**
5
+ * Per-request transport options.
6
+ *
7
+ * Declared structurally rather than imported so this package builds against the
8
+ * currently published client; it matches `SendOptions` there exactly.
9
+ */
10
+ export interface SendOptions {
11
+ signal?: AbortSignal;
12
+ timeoutMs?: number;
13
+ retry?: false | {
14
+ maxAttempts?: number;
15
+ };
16
+ }
17
+ /**
18
+ * What the provider hands out.
19
+ *
20
+ * An interface rather than `GeoPlacesClient` because the provider wraps the
21
+ * real client to refresh tokens first, and a class with private fields is not
22
+ * structurally assignable — which is why this used to be an `Object.create`
23
+ * prototype hack.
24
+ */
25
+ export interface LocationClient {
26
+ readonly config: {
27
+ serviceId: string;
28
+ };
29
+ send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
30
+ /**
31
+ * This application's own configuration, read from the access token —
32
+ * bias precision, and the countries it is scoped to (api#65).
33
+ *
34
+ * Here so a React app can SHOW its own settings: populate a country
35
+ * selector with the markets it serves, label a settings screen. Being a
36
+ * few minutes stale is cosmetic for that.
37
+ *
38
+ * It is not an entitlement check, and the `countries` value must not be
39
+ * used to shape requests. The token is a snapshot; the API reads the scope
40
+ * fresh from the application row on every call. Injecting a stale scope
41
+ * turns a request that would have succeeded into a 400. See
42
+ * `AppConfigClaims` in @chaosity/location-client for the measurement.
43
+ *
44
+ * Returns `{}` when the token carries no application config, which is the
45
+ * case until one is set in the portal.
46
+ */
47
+ getAppConfig(): AppConfigClaims;
48
+ }
49
+ interface LocationClientContextValue {
50
+ client: LocationClient | null;
51
+ getToken: () => string | undefined;
52
+ loading: boolean;
53
+ error: string | null;
54
+ }
55
+ export interface LocationClientProviderProps {
56
+ children: ReactNode;
57
+ getConfig: () => Promise<ClientConfig & {
58
+ expiresAt?: number;
59
+ }>;
60
+ }
61
+ export declare function LocationClientProvider({ children, getConfig, }: LocationClientProviderProps): import("react").JSX.Element;
62
+ export declare function useLocationClient(): LocationClientContextValue;
63
+ export {};
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ 'use client';
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.LocationClientProvider = LocationClientProvider;
8
+ exports.useLocationClient = useLocationClient;
9
+ const jsx_runtime_1 = require("react/jsx-runtime");
10
+ const location_client_1 = require("@chaosity/location-client");
11
+ const debug_1 = __importDefault(require("debug"));
12
+ const react_1 = require("react");
13
+ const log = (0, debug_1.default)('location-client-react:provider');
14
+ const LocationClientContext = (0, react_1.createContext)(undefined);
15
+ const DEFAULT_LIFETIME_MS = 900000;
16
+ /**
17
+ * When this token needs replacing.
18
+ *
19
+ * The `exp` claim first — it is the only value that cannot disagree with what
20
+ * the API will accept, and the server-side TokenProvider reads the same one.
21
+ * `expiresAt` is whatever `getConfig` chose to report, and the final fallback
22
+ * is a guess used only when the token cannot be parsed at all.
23
+ */
24
+ function expiryOf(cfg) {
25
+ return ((0, location_client_1.readTokenExpiry)(cfg.token) ??
26
+ cfg.expiresAt ??
27
+ Date.now() + DEFAULT_LIFETIME_MS);
28
+ }
29
+ function LocationClientProvider({ children, getConfig, }) {
30
+ const [client, setClient] = (0, react_1.useState)(null);
31
+ const [loading, setLoading] = (0, react_1.useState)(true);
32
+ const [error, setError] = (0, react_1.useState)(null);
33
+ const tokenRef = (0, react_1.useRef)(undefined);
34
+ const expiresAtRef = (0, react_1.useRef)(null);
35
+ const getConfigRef = (0, react_1.useRef)(getConfig);
36
+ const refreshPromiseRef = (0, react_1.useRef)(null);
37
+ const timerRef = (0, react_1.useRef)(null);
38
+ const mountedRef = (0, react_1.useRef)(true);
39
+ (0, react_1.useEffect)(() => {
40
+ getConfigRef.current = getConfig;
41
+ }, [getConfig]);
42
+ const isTokenExpired = (0, react_1.useCallback)(() => {
43
+ if (!expiresAtRef.current)
44
+ return true;
45
+ return (Date.now() >= expiresAtRef.current - location_client_1.TOKEN_REFRESH_BUFFER_SECONDS * 1000);
46
+ }, []);
47
+ /**
48
+ * Refresh once, however many callers ask at the same moment.
49
+ *
50
+ * REJECTS on failure. It used to swallow the error into state and resolve,
51
+ * so `send` carried on with the token it already had — guaranteeing a 401 on
52
+ * the very next call and reporting it as an API error rather than a refresh
53
+ * failure.
54
+ */
55
+ const refreshToken = (0, react_1.useCallback)(async () => {
56
+ if (refreshPromiseRef.current)
57
+ return refreshPromiseRef.current;
58
+ refreshPromiseRef.current = (async () => {
59
+ const cfg = await getConfigRef.current();
60
+ tokenRef.current = cfg.token;
61
+ expiresAtRef.current = expiryOf(cfg);
62
+ log('Token refreshed (expires in %ds)', Math.floor((expiresAtRef.current - Date.now()) / 1000));
63
+ if (mountedRef.current)
64
+ setError(null);
65
+ })();
66
+ try {
67
+ await refreshPromiseRef.current;
68
+ scheduleRefreshRef.current();
69
+ }
70
+ catch (err) {
71
+ const message = err instanceof Error ? err.message : 'Failed to refresh token';
72
+ log('Token refresh failed: %s', message);
73
+ if (mountedRef.current)
74
+ setError(message);
75
+ throw err;
76
+ }
77
+ finally {
78
+ refreshPromiseRef.current = null;
79
+ }
80
+ }, []);
81
+ /**
82
+ * Refresh AHEAD of expiry, on a timer.
83
+ *
84
+ * This is the whole fix for the map path. MapLibre's `transformRequest` is
85
+ * synchronous by contract, so `getToken` cannot await anything — the token it
86
+ * reads has to be valid already. Refresh used to happen only inside the `send`
87
+ * wrapper, which the map never calls: it requests tiles, glyphs and sprites
88
+ * directly. So after 15 minutes every map request failed, for as long as the
89
+ * page stayed open, and no amount of panning recovered it.
90
+ */
91
+ const scheduleRefresh = (0, react_1.useCallback)(() => {
92
+ if (timerRef.current)
93
+ clearTimeout(timerRef.current);
94
+ if (!expiresAtRef.current)
95
+ return;
96
+ const delay = Math.max(0, expiresAtRef.current - location_client_1.TOKEN_REFRESH_BUFFER_SECONDS * 1000 - Date.now());
97
+ log('Next refresh in %ds', Math.floor(delay / 1000));
98
+ timerRef.current = setTimeout(() => {
99
+ // Errors are already surfaced onto state by refreshToken; swallow here so
100
+ // a failed background refresh cannot become an unhandled rejection.
101
+ void refreshToken().catch(() => { });
102
+ }, delay);
103
+ }, [refreshToken]);
104
+ // refreshToken and scheduleRefresh reference each other; a ref breaks the cycle
105
+ // without recreating either callback on every render.
106
+ const scheduleRefreshRef = (0, react_1.useRef)(() => { });
107
+ (0, react_1.useEffect)(() => {
108
+ scheduleRefreshRef.current = scheduleRefresh;
109
+ }, [scheduleRefresh]);
110
+ /**
111
+ * Synchronous read for the map path.
112
+ *
113
+ * If the token is already stale — a timer that never fired because the tab was
114
+ * backgrounded and throttled — this kicks off a refresh but cannot wait for it.
115
+ * The current read still returns the stale value; the point is that the NEXT
116
+ * one will not.
117
+ */
118
+ const getToken = (0, react_1.useCallback)(() => {
119
+ // `tokenRef.current` guards the pre-initialisation window: until the first
120
+ // config load lands there is no expiry to judge, and firing here would race
121
+ // the initial fetch and request a second token nobody asked for.
122
+ if (tokenRef.current && isTokenExpired() && !refreshPromiseRef.current) {
123
+ log('Stale token read — refreshing in the background');
124
+ void refreshToken().catch(() => { });
125
+ }
126
+ return tokenRef.current;
127
+ }, [isTokenExpired, refreshToken]);
128
+ const ensureValidToken = (0, react_1.useCallback)(async () => {
129
+ if (!isTokenExpired())
130
+ return;
131
+ await refreshToken();
132
+ }, [isTokenExpired, refreshToken]);
133
+ const ensureValidTokenRef = (0, react_1.useRef)(ensureValidToken);
134
+ (0, react_1.useEffect)(() => {
135
+ ensureValidTokenRef.current = ensureValidToken;
136
+ }, [ensureValidToken]);
137
+ /**
138
+ * A backgrounded tab has its timers throttled, so the scheduled refresh can be
139
+ * arbitrarily late. Refresh on the way back in, before the user touches the map.
140
+ */
141
+ (0, react_1.useEffect)(() => {
142
+ if (typeof document === 'undefined')
143
+ return;
144
+ const onVisible = () => {
145
+ if (document.visibilityState === 'visible' &&
146
+ tokenRef.current &&
147
+ isTokenExpired()) {
148
+ log('Tab visible again with a stale token — refreshing');
149
+ void refreshToken().catch(() => { });
150
+ }
151
+ };
152
+ document.addEventListener('visibilitychange', onVisible);
153
+ return () => document.removeEventListener('visibilitychange', onVisible);
154
+ }, [isTokenExpired, refreshToken]);
155
+ (0, react_1.useEffect)(() => {
156
+ mountedRef.current = true;
157
+ log('Initializing LocationClientProvider');
158
+ getConfigRef
159
+ .current()
160
+ .then((cfg) => {
161
+ if (!mountedRef.current)
162
+ return;
163
+ tokenRef.current = cfg.token;
164
+ expiresAtRef.current = expiryOf(cfg);
165
+ const baseClient = new location_client_1.GeoPlacesClient({
166
+ apiUrl: cfg.apiUrl,
167
+ token: cfg.token,
168
+ getToken,
169
+ /**
170
+ * The 401 escape hatch (#19).
171
+ *
172
+ * Covers what the timer cannot: a token revoked from the portal, or
173
+ * minted against a client secret since rotated, is refused by the API
174
+ * while still minutes from its own `exp` — so nothing on this side has
175
+ * any reason to replace it, and every request fails until the buffer
176
+ * finally comes around. `getToken` cannot help, being synchronous.
177
+ *
178
+ * The client awaits this after a 401 and retries the request once with
179
+ * what it returns; the same token, or nothing, means no retry, so a
180
+ * doomed request is never sent — or billed — twice.
181
+ *
182
+ * It REJECTS when the refresh itself fails, and that is left to
183
+ * propagate out of `send` deliberately: the consumer learns the token
184
+ * endpoint is down rather than being told the API rejected them. Same
185
+ * answer the pre-flight `ensureValidToken` path already gives.
186
+ */
187
+ refreshToken: async () => {
188
+ await refreshToken();
189
+ return tokenRef.current;
190
+ },
191
+ });
192
+ // A plain object, not Object.create(baseClient): the prototype hack was
193
+ // opaque, and its `send` dropped the second argument entirely — so once
194
+ // the client gained `signal`/`timeoutMs`, every option passed through
195
+ // this provider would have been silently discarded.
196
+ const refreshing = {
197
+ config: baseClient.config,
198
+ async send(command, options) {
199
+ await ensureValidTokenRef.current();
200
+ return baseClient.send(command, options);
201
+ },
202
+ // Reads whatever token the client currently holds. Deliberately not
203
+ // awaiting a refresh: this is display data, callers expect it to be
204
+ // synchronous, and a token that is minutes from expiry carries the
205
+ // same application config as its replacement will.
206
+ getAppConfig() {
207
+ return baseClient.getAppConfig();
208
+ },
209
+ };
210
+ setClient(refreshing);
211
+ scheduleRefreshRef.current();
212
+ log('Client initialized (token expires in %ds)', Math.floor((expiresAtRef.current - Date.now()) / 1000));
213
+ setLoading(false);
214
+ })
215
+ .catch((err) => {
216
+ if (!mountedRef.current)
217
+ return;
218
+ const message = err instanceof Error ? err.message : 'Failed to initialize client';
219
+ log('Initialization failed: %s', message);
220
+ setError(message);
221
+ setLoading(false);
222
+ });
223
+ return () => {
224
+ mountedRef.current = false;
225
+ if (timerRef.current)
226
+ clearTimeout(timerRef.current);
227
+ };
228
+ }, [getToken, refreshToken]);
229
+ return ((0, jsx_runtime_1.jsx)(LocationClientContext.Provider, { value: { client, getToken, loading, error }, children: children }));
230
+ }
231
+ function useLocationClient() {
232
+ const context = (0, react_1.useContext)(LocationClientContext);
233
+ if (context === undefined) {
234
+ throw new Error('useLocationClient must be used within LocationClientProvider');
235
+ }
236
+ return context;
237
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { LocationClientProvider, useLocationClient, } from './provider/LocationClientProvider';
2
- export type { LocationClient, LocationClientProviderProps, SendOptions, } from './provider/LocationClientProvider';
3
- export { useMapLanguage } from './hooks/useMapLanguage';
1
+ export { LocationClientProvider, useLocationClient, } from './provider/LocationClientProvider.js';
2
+ export type { LocationClient, LocationClientProviderProps, SendOptions, } from './provider/LocationClientProvider.js';
3
+ export { useMapLanguage } from './hooks/useMapLanguage.js';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { LocationClientProvider, useLocationClient, } from './provider/LocationClientProvider';
2
- export { useMapLanguage } from './hooks/useMapLanguage';
1
+ export { LocationClientProvider, useLocationClient, } from './provider/LocationClientProvider.js';
2
+ export { useMapLanguage } from './hooks/useMapLanguage.js';
@@ -159,6 +159,28 @@ export function LocationClientProvider({ children, getConfig, }) {
159
159
  apiUrl: cfg.apiUrl,
160
160
  token: cfg.token,
161
161
  getToken,
162
+ /**
163
+ * The 401 escape hatch (#19).
164
+ *
165
+ * Covers what the timer cannot: a token revoked from the portal, or
166
+ * minted against a client secret since rotated, is refused by the API
167
+ * while still minutes from its own `exp` — so nothing on this side has
168
+ * any reason to replace it, and every request fails until the buffer
169
+ * finally comes around. `getToken` cannot help, being synchronous.
170
+ *
171
+ * The client awaits this after a 401 and retries the request once with
172
+ * what it returns; the same token, or nothing, means no retry, so a
173
+ * doomed request is never sent — or billed — twice.
174
+ *
175
+ * It REJECTS when the refresh itself fails, and that is left to
176
+ * propagate out of `send` deliberately: the consumer learns the token
177
+ * endpoint is down rather than being told the API rejected them. Same
178
+ * answer the pre-flight `ensureValidToken` path already gives.
179
+ */
180
+ refreshToken: async () => {
181
+ await refreshToken();
182
+ return tokenRef.current;
183
+ },
162
184
  });
163
185
  // A plain object, not Object.create(baseClient): the prototype hack was
164
186
  // opaque, and its `send` dropped the second argument entirely — so once
@@ -196,7 +218,7 @@ export function LocationClientProvider({ children, getConfig, }) {
196
218
  if (timerRef.current)
197
219
  clearTimeout(timerRef.current);
198
220
  };
199
- }, [getToken]);
221
+ }, [getToken, refreshToken]);
200
222
  return (_jsx(LocationClientContext.Provider, { value: { client, getToken, loading, error }, children: children }));
201
223
  }
202
224
  export function useLocationClient() {
package/package.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@chaosity/location-client-react",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "React bindings for Chaosity Location Service client",
5
5
  "type": "module",
6
- "main": "dist/index.js",
6
+ "main": "dist/cjs/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
- "build": "tsc",
9
+ "build": "tsc && tsc -p tsconfig.cjs.json && node scripts/finish-cjs.mjs",
10
10
  "dev": "tsc --watch",
11
11
  "lint": "eslint . && prettier --check .",
12
12
  "lint:fix": "eslint --fix . && prettier --write .",
13
13
  "format": "prettier --write .",
14
14
  "test": "vitest run",
15
15
  "test:watch": "vitest",
16
- "prepublishOnly": "npm run build",
17
- "prepare": "husky"
16
+ "prepublishOnly": "npm run build && npm run smoke",
17
+ "prepare": "husky",
18
+ "smoke": "node scripts/smoke.mjs"
18
19
  },
19
20
  "keywords": [
20
21
  "react",
@@ -47,7 +48,7 @@
47
48
  }
48
49
  },
49
50
  "devDependencies": {
50
- "@chaosity/location-client": "^0.4.0",
51
+ "@chaosity/location-client": "^0.7.0",
51
52
  "@eslint/js": "^10.0.1",
52
53
  "@testing-library/react": "^16.0.0",
53
54
  "@testing-library/user-event": "^14.0.0",
@@ -71,5 +72,22 @@
71
72
  "files": [
72
73
  "dist",
73
74
  "README.md"
74
- ]
75
+ ],
76
+ "exports": {
77
+ ".": {
78
+ "import": {
79
+ "types": "./dist/index.d.ts",
80
+ "default": "./dist/index.js"
81
+ },
82
+ "require": {
83
+ "types": "./dist/cjs/index.d.ts",
84
+ "default": "./dist/cjs/index.js"
85
+ }
86
+ }
87
+ },
88
+ "module": "dist/index.js",
89
+ "sideEffects": false,
90
+ "engines": {
91
+ "node": ">=20"
92
+ }
75
93
  }