@findlane/server 0.4.0 → 0.4.1

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/dist/index.cjs ADDED
@@ -0,0 +1,240 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/index.ts
40
+ var exports_src = {};
41
+ __export(exports_src, {
42
+ FindlaneError: () => FindlaneError,
43
+ SearchServerClient: () => SearchServerClient,
44
+ StagedReplacementError: () => StagedReplacementError,
45
+ defaultBaseUrl: () => defaultBaseUrl
46
+ });
47
+ module.exports = __toCommonJS(exports_src);
48
+ function pause(ms, signal) {
49
+ return new Promise((resolve, reject) => {
50
+ const onAbort = () => {
51
+ clearTimeout(timer);
52
+ reject(signal?.reason);
53
+ };
54
+ const timer = setTimeout(() => {
55
+ signal?.removeEventListener("abort", onAbort);
56
+ resolve();
57
+ }, ms);
58
+ signal?.addEventListener("abort", onAbort, { once: true });
59
+ });
60
+ }
61
+
62
+ class StagedReplacementError extends Error {
63
+ stageId;
64
+ constructor(stageId, cause) {
65
+ super(`Staged replacement ${stageId} failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
66
+ this.stageId = stageId;
67
+ this.name = "StagedReplacementError";
68
+ }
69
+ }
70
+
71
+ class FindlaneError extends Error {
72
+ status;
73
+ constructor(status, message) {
74
+ super(message);
75
+ this.status = status;
76
+ this.name = "FindlaneError";
77
+ }
78
+ }
79
+ var defaultBaseUrl = "https://api.findlane.dev";
80
+
81
+ class SearchServerClient {
82
+ baseUrl;
83
+ workspaceId;
84
+ key;
85
+ fetcher;
86
+ constructor(options) {
87
+ this.baseUrl = (options.baseUrl ?? defaultBaseUrl).replace(/\/$/, "");
88
+ this.workspaceId = options.workspaceId;
89
+ this.key = options.apiKey;
90
+ this.fetcher = options.fetch ?? fetch;
91
+ if (!this.baseUrl || !this.workspaceId || !this.key)
92
+ throw new Error("workspaceId and apiKey are required");
93
+ }
94
+ index(name) {
95
+ const path = `/api/teams/${encodeURIComponent(this.workspaceId)}/indexes/${encodeURIComponent(name)}`;
96
+ const request = async (suffix, method = "GET", payload, signal) => {
97
+ const response = await this.fetcher(this.baseUrl + path + suffix, {
98
+ method,
99
+ headers: {
100
+ authorization: `Bearer ${this.key}`,
101
+ ...payload === undefined ? {} : { "content-type": "application/json" }
102
+ },
103
+ body: payload === undefined ? undefined : JSON.stringify(payload),
104
+ signal
105
+ });
106
+ const data = await response.json();
107
+ if (!response.ok)
108
+ throw new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
109
+ return data;
110
+ };
111
+ const retry = async (work, signal) => {
112
+ for (let attempt = 0;; attempt++) {
113
+ signal?.throwIfAborted();
114
+ try {
115
+ return await work();
116
+ } catch (error) {
117
+ if (attempt >= 3 || error instanceof FindlaneError && ![408, 429, 502, 503, 504].includes(error.status))
118
+ throw error;
119
+ await new Promise((resolve, reject) => {
120
+ const onAbort = () => {
121
+ clearTimeout(timer);
122
+ reject(signal?.reason);
123
+ };
124
+ const timer = setTimeout(() => {
125
+ signal?.removeEventListener("abort", onAbort);
126
+ resolve();
127
+ }, 100 * 2 ** attempt);
128
+ signal?.addEventListener("abort", onAbort, { once: true });
129
+ });
130
+ }
131
+ }
132
+ };
133
+ const stagePath = (stageId) => `/staged-replacements/${encodeURIComponent(stageId)}`;
134
+ const beginReplacement = (stageId, signal) => retry(() => request(stagePath(stageId), "POST", undefined, signal), signal);
135
+ const uploadReplacementBatch = (stageId, batchId, records, signal) => retry(() => request(`${stagePath(stageId)}/batches/${encodeURIComponent(batchId)}`, "POST", { records }, signal), signal);
136
+ const commitReplacement = (stageId, expectedBatches, expectedRecords, signal) => retry(() => request(`${stagePath(stageId)}/commit`, "POST", { expectedBatches, expectedRecords }, signal), signal);
137
+ const writeOne = (operation, objectID, record, options) => {
138
+ if (!Number.isSafeInteger(options.expectedVersion) || options.expectedVersion < 0)
139
+ throw new RangeError("expectedVersion must be a non-negative integer");
140
+ const idempotencyKey = options.idempotencyKey ?? globalThis.crypto.randomUUID();
141
+ const payload = { operation, objectID, ...record === undefined ? {} : { record }, expectedVersion: options.expectedVersion, idempotencyKey };
142
+ return retry(() => request("/record-writes", "POST", payload, options.signal), options.signal);
143
+ };
144
+ return {
145
+ search: (query = {}, signal) => request("/search", "POST", query, signal),
146
+ suggest: async (query, signal) => (await request("/suggest", "POST", query, signal)).didYouMean,
147
+ upsert: (records, signal) => request("/records", "POST", { records }, signal),
148
+ getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
149
+ upsertOne: (record, options) => writeOne("upsert", record.objectID, record, options),
150
+ deleteOne: (objectID, options) => writeOne("delete", objectID, undefined, options),
151
+ replaceAll: (records, signal) => request("/records", "PUT", { records }, signal),
152
+ beginReplacement,
153
+ uploadReplacementBatch,
154
+ commitReplacement,
155
+ abortReplacement: (stageId, signal) => retry(() => request(stagePath(stageId), "DELETE", undefined, signal), signal),
156
+ replaceAllStaged: async (records, options = {}) => {
157
+ const stageId = options.stageId ?? globalThis.crypto.randomUUID();
158
+ const batchSize = options.batchSize ?? 500;
159
+ const maxBatchBytes = options.maxBatchBytes ?? 8 * 1024 * 1024;
160
+ if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 1000)
161
+ throw new RangeError("batchSize must be between 1 and 1,000");
162
+ if (!Number.isInteger(maxBatchBytes) || maxBatchBytes < 32 || maxBatchBytes > 32 * 1024 * 1024)
163
+ throw new RangeError("maxBatchBytes must be between 32 bytes and 32 MiB");
164
+ try {
165
+ const stage = await beginReplacement(stageId, options.signal);
166
+ if (stage.status === "committed")
167
+ return { acceptedRevision: stage.acceptedRevision, recordCount: stage.recordCount, replaced: true };
168
+ if (stage.status !== "open")
169
+ throw new Error(`Stage is ${stage.status}`);
170
+ let batch = [];
171
+ let batchBytes = 14;
172
+ let batchCount = 0;
173
+ let recordCount = 0;
174
+ const upload = async () => {
175
+ if (batch.length === 0)
176
+ return;
177
+ await uploadReplacementBatch(stageId, String(batchCount), batch, options.signal);
178
+ recordCount += batch.length;
179
+ batchCount++;
180
+ batch = [];
181
+ batchBytes = 14;
182
+ };
183
+ for await (const record of records) {
184
+ options.signal?.throwIfAborted();
185
+ const recordBytes = new TextEncoder().encode(JSON.stringify(record)).byteLength;
186
+ if (recordBytes + 14 > maxBatchBytes)
187
+ throw new RangeError(`A record exceeds maxBatchBytes (${maxBatchBytes})`);
188
+ if (batch.length && (batch.length >= batchSize || batchBytes + recordBytes + 1 > maxBatchBytes))
189
+ await upload();
190
+ batch.push(record);
191
+ batchBytes += recordBytes + 1;
192
+ }
193
+ await upload();
194
+ return await commitReplacement(stageId, batchCount, recordCount, options.signal);
195
+ } catch (error) {
196
+ throw new StagedReplacementError(stageId, error);
197
+ }
198
+ },
199
+ delete: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "DELETE", undefined, signal),
200
+ getSettings: (signal) => request("/config", "GET", undefined, signal),
201
+ replaceSettings: (settings, signal) => {
202
+ const { rebuild: _rebuild, rebuilt: _rebuilt, applied: _applied, ...body } = settings;
203
+ return request("/config", "PUT", body, signal);
204
+ },
205
+ waitForSettings: async (options = {}) => {
206
+ const deadline = Date.now() + (options.timeoutMs ?? 30 * 60000);
207
+ while (true) {
208
+ options.signal?.throwIfAborted();
209
+ const settings = await request("/config", "GET", undefined, options.signal);
210
+ if (settings.rebuild?.status === "failed")
211
+ throw new Error(`Settings rebuild failed: ${settings.rebuild.message}`);
212
+ if (!settings.rebuild)
213
+ return settings;
214
+ if (Date.now() >= deadline)
215
+ throw new Error("Timed out waiting for the settings rebuild");
216
+ await pause(Math.min(options.intervalMs ?? 1000, deadline - Date.now()), options.signal);
217
+ }
218
+ },
219
+ getPublicSearchAccess: (signal) => request("/public-access", "GET", undefined, signal),
220
+ setPublicSearchPolicy: (policy, signal) => request("/public-access/policy", "PUT", { policy }, signal),
221
+ usePublicIndexDefaults: (signal) => request("/public-access/policy", "DELETE", undefined, signal),
222
+ getIndexingStatus: (signal) => request("/indexing", "GET", undefined, signal),
223
+ waitUntilIndexed: async (revision, options = {}) => {
224
+ const deadline = Date.now() + (options.timeoutMs ?? 30000);
225
+ const interval = options.intervalMs ?? 100;
226
+ while (true) {
227
+ options.signal?.throwIfAborted();
228
+ const status = await request("/indexing", "GET", undefined, options.signal);
229
+ if (status.lastError)
230
+ throw new Error(status.lastError);
231
+ if (status.indexedRevision >= revision)
232
+ return status;
233
+ if (Date.now() >= deadline)
234
+ throw new Error(`Timed out waiting for revision ${revision}`);
235
+ await pause(Math.min(interval, deadline - Date.now()), options.signal);
236
+ }
237
+ }
238
+ };
239
+ }
240
+ }
@@ -0,0 +1,243 @@
1
+ export type JsonRecord = {
2
+ objectID: string;
3
+ [key: string]: unknown;
4
+ };
5
+ export type SortRule = {
6
+ field: string;
7
+ direction: "asc" | "desc";
8
+ };
9
+ export type SortOption = SortRule & {
10
+ id: string;
11
+ label: string;
12
+ thenBy: SortRule[];
13
+ };
14
+ export type IndexSettings = {
15
+ /** One to five fields, matched with their weight. */
16
+ searchableAttributes: {
17
+ field: string;
18
+ weight: number;
19
+ }[];
20
+ facetFields: string[];
21
+ filterFields?: string[];
22
+ typoTolerance: boolean;
23
+ sorts: SortOption[];
24
+ defaultSort: string;
25
+ /** Orders equally relevant results (within 10% of the best score) by up to three sort fields. */
26
+ customRanking?: SortRule[];
27
+ /** Groups of interchangeable words or phrases (up to four words each). */
28
+ synonyms?: string[][];
29
+ /** Languages whose common words queries drop, and whose plurals and alternative spellings match. */
30
+ languages?: Array<"da" | "de" | "en" | "is" | "no" | "sv">;
31
+ /** Searches say which words typo tolerance corrected, and `suggest()` offers a corrected query for a
32
+ * search that finds nothing. Off by default. */
33
+ didYouMean?: boolean;
34
+ normalizationProfile?: "default-v2" | "default-v1" | "icelandic-ascii-v1";
35
+ [key: string]: unknown;
36
+ };
37
+ /** A settings change running in the background (with the settings it applies), or the last one's
38
+ * failure. `kind` is "searchable" (new searchable fields filled in) or "rebuild" (a full copy). */
39
+ export type SettingsRebuild = {
40
+ status: "running";
41
+ kind: "searchable" | "rebuild";
42
+ copied: number;
43
+ total: number;
44
+ startedAt: string;
45
+ updatedAt: string;
46
+ settings: IndexSettings;
47
+ } | {
48
+ status: "failed";
49
+ code: number;
50
+ message: string;
51
+ finishedAt: string;
52
+ settings: IndexSettings;
53
+ };
54
+ /** The active settings, and a rebuild that will replace them (or the last one's failure). */
55
+ export type CurrentSettings = IndexSettings & {
56
+ rebuild: SettingsRebuild | null;
57
+ };
58
+ /** How saved settings were applied: "none" (unchanged), "instant" (query-time settings, and removed
59
+ * or reordered searchable fields), "fields" (facet, filter, sort, and ranking fields filled in
60
+ * place), "searchable" (new searchable fields filled in, after any field changes), or "rebuild" (text
61
+ * normalization). The last two continue in the background when large:
62
+ * `rebuilt` is false and `rebuild` shows progress; searches use the previous settings until then. */
63
+ export type SavedSettings = IndexSettings & {
64
+ applied: "none" | "instant" | "fields" | "searchable" | "rebuild";
65
+ rebuilt: boolean;
66
+ rebuild: SettingsRebuild | null;
67
+ };
68
+ export type PublicSearchPolicy = {
69
+ returnFields: string[];
70
+ searchFields: string[];
71
+ filterFields: string[];
72
+ facetFields: string[];
73
+ sortIds: string[];
74
+ defaultSort: string;
75
+ };
76
+ export type PublicSearchAccess = {
77
+ publicId: string;
78
+ allowedOrigins: string[];
79
+ policy: PublicSearchPolicy | null;
80
+ };
81
+ export type WriteReceipt = {
82
+ acceptedRevision: number;
83
+ recordCount: number;
84
+ recordVersion?: number;
85
+ [key: string]: unknown;
86
+ };
87
+ export type RecordSnapshot<T> = {
88
+ objectID: string;
89
+ version: number;
90
+ record: T | null;
91
+ };
92
+ export type SearchRequest = {
93
+ q?: string;
94
+ /** A sort ID from the index settings, or "relevance". */
95
+ sort?: string;
96
+ /** Field values to keep: a value, `{ in: [...] }`, or a range `{ gte, lte }` for numbers and dates. */
97
+ filters?: Record<string, unknown>;
98
+ facets?: string[];
99
+ /** Facets (also in `facets`) counted without the filter on their own field, so a list of choices
100
+ * keeps its other values, with counts, after one is chosen. */
101
+ disjunctiveFacets?: string[];
102
+ /** Number fields (up to eight) whose smallest and largest values across the matches to return in
103
+ * `facetStats`, such as a price slider's bounds. Each field's own filter is left out. */
104
+ facetStats?: string[];
105
+ limit?: number;
106
+ offset?: number;
107
+ attributesToRetrieve?: string[];
108
+ /** "page" (default) counts only the returned page when a query matches more; "exact" counts every match. */
109
+ countMode?: "page" | "exact";
110
+ /** "page" (default) counts facet values in the returned page; "exact" counts them across every match. */
111
+ facetMode?: "page" | "exact";
112
+ /** Mark the words the query matched in each hit's searchable fields: true for <mark> tags, or custom tags.
113
+ * Highlighted values are HTML-escaped, so they can be inserted as HTML. */
114
+ highlight?: boolean | {
115
+ preTag?: string;
116
+ postTag?: string;
117
+ };
118
+ };
119
+ export type SearchResponse<T> = {
120
+ hits: {
121
+ id: string;
122
+ score: number | null;
123
+ record: T;
124
+ /** With `highlight`: matched searchable fields by path; list fields give one string per value. */
125
+ highlights?: Record<string, string | string[]>;
126
+ }[];
127
+ total: number;
128
+ /** "gte" when `total` is a lower bound (page count mode). */
129
+ totalRelation?: "eq" | "gte";
130
+ hasMore?: boolean;
131
+ facets: Record<string, Record<string, number>>;
132
+ /** With `facetStats`: each field's smallest and largest value across the matches, without the field's
133
+ * own filter. A field no match has is left out. */
134
+ facetStats?: Record<string, {
135
+ min: number;
136
+ max: number;
137
+ }>;
138
+ tookMs: number;
139
+ sort?: string;
140
+ /** With the index's `didYouMean` setting: the query with the words typo tolerance corrected, when the
141
+ * results matched some words only by typo ("Showing results for …"), else null. */
142
+ didYouMean?: DidYouMean | null;
143
+ };
144
+ /** A corrected query. `applied`: the results are already for `q` (a search's "Showing results for");
145
+ * otherwise searching `q` finds results that the query didn't ("Did you mean"). `changes` lists each
146
+ * changed part of the query as typed. */
147
+ export type DidYouMean = {
148
+ q: string;
149
+ applied: boolean;
150
+ changes: {
151
+ from: string;
152
+ to: string;
153
+ }[];
154
+ };
155
+ export type ConditionalWriteOptions = {
156
+ expectedVersion: number;
157
+ idempotencyKey?: string;
158
+ signal?: AbortSignal;
159
+ };
160
+ export type IndexingStatus = {
161
+ acceptedRevision: number;
162
+ indexedRevision: number;
163
+ lastError?: string | null;
164
+ [key: string]: unknown;
165
+ };
166
+ export type ReplacementStage = {
167
+ stageId: string;
168
+ status: "open" | "committed" | "aborted";
169
+ baseRevision: number;
170
+ recordCount: number;
171
+ batchCount: number;
172
+ acceptedRevision: number | null;
173
+ };
174
+ export type StagedReplaceOptions = {
175
+ stageId?: string;
176
+ /** Records per batch: 1–1,000 (default 500). */
177
+ batchSize?: number;
178
+ /** Bytes per batch request (default 8 MiB). */
179
+ maxBatchBytes?: number;
180
+ signal?: AbortSignal;
181
+ };
182
+ export declare class StagedReplacementError extends Error {
183
+ stageId: string;
184
+ constructor(stageId: string, cause: unknown);
185
+ }
186
+ export declare class FindlaneError extends Error {
187
+ status: number;
188
+ constructor(status: number, message: string);
189
+ }
190
+ /** The hosted Findlane API, used unless `baseUrl` says otherwise. */
191
+ export declare const defaultBaseUrl = "https://api.findlane.dev";
192
+ export type ServerClientOptions = {
193
+ /** The API to call; defaults to the hosted Findlane API. */
194
+ baseUrl?: string;
195
+ workspaceId: string;
196
+ apiKey: string;
197
+ fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
198
+ };
199
+ export declare class SearchServerClient {
200
+ private readonly baseUrl;
201
+ private readonly workspaceId;
202
+ private readonly key;
203
+ private readonly fetcher;
204
+ constructor(options: ServerClientOptions);
205
+ index<T extends {
206
+ objectID: string;
207
+ } = JsonRecord>(name: string): {
208
+ /** Searches the index; the API key needs the `search` scope. */
209
+ search: <R = T>(query?: SearchRequest, signal?: AbortSignal) => Promise<SearchResponse<R>>;
210
+ /** Did you mean: for a search that found nothing, a query that finds something, or null. Takes the
211
+ * same request (its q, filters, and fields matter). Needs the `didYouMean` setting; not counted as
212
+ * a search. */
213
+ suggest: (query: SearchRequest, signal?: AbortSignal) => Promise<DidYouMean | null>;
214
+ upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
215
+ getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
216
+ upsertOne: (record: T, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
217
+ deleteOne: (objectID: string, options: ConditionalWriteOptions) => Promise<WriteReceipt>;
218
+ replaceAll: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
219
+ beginReplacement: (stageId: string, signal?: AbortSignal) => Promise<ReplacementStage>;
220
+ uploadReplacementBatch: (stageId: string, batchId: string, records: T[], signal?: AbortSignal) => Promise<ReplacementStage>;
221
+ commitReplacement: (stageId: string, expectedBatches: number, expectedRecords: number, signal?: AbortSignal) => Promise<WriteReceipt>;
222
+ abortReplacement: (stageId: string, signal?: AbortSignal) => Promise<ReplacementStage>;
223
+ replaceAllStaged: (records: Iterable<T> | AsyncIterable<T>, options?: StagedReplaceOptions) => Promise<WriteReceipt>;
224
+ delete: (objectID: string, signal?: AbortSignal) => Promise<WriteReceipt>;
225
+ getSettings: (signal?: AbortSignal) => Promise<CurrentSettings>;
226
+ replaceSettings: (settings: IndexSettings, signal?: AbortSignal) => Promise<SavedSettings>;
227
+ /** Waits until no settings rebuild is running; throws if it failed. */
228
+ waitForSettings: (options?: {
229
+ timeoutMs?: number;
230
+ intervalMs?: number;
231
+ signal?: AbortSignal;
232
+ }) => Promise<CurrentSettings>;
233
+ getPublicSearchAccess: (signal?: AbortSignal) => Promise<PublicSearchAccess>;
234
+ setPublicSearchPolicy: (policy: PublicSearchPolicy, signal?: AbortSignal) => Promise<PublicSearchAccess>;
235
+ usePublicIndexDefaults: (signal?: AbortSignal) => Promise<PublicSearchAccess>;
236
+ getIndexingStatus: (signal?: AbortSignal) => Promise<IndexingStatus>;
237
+ waitUntilIndexed: (revision: number, options?: {
238
+ timeoutMs?: number;
239
+ intervalMs?: number;
240
+ signal?: AbortSignal;
241
+ }) => Promise<IndexingStatus>;
242
+ };
243
+ }
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@findlane/server",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
5
5
  "keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
6
6
  "license": "MIT",
7
7
  "homepage": "https://findlane.dev/docs/server-sdk",
8
8
  "type": "module",
9
- "main": "./dist/index.js",
9
+ "main": "./dist/index.cjs",
10
+ "module": "./dist/index.js",
10
11
  "types": "./dist/index.d.ts",
11
12
  "exports": {
12
13
  ".": {
13
- "types": "./dist/index.d.ts",
14
- "import": "./dist/index.js"
14
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
15
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
15
16
  },
16
17
  "./package.json": "./package.json"
17
18
  },
@@ -20,7 +21,7 @@
20
21
  "engines": { "node": ">=18" },
21
22
  "publishConfig": { "access": "public" },
22
23
  "scripts": {
23
- "build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --outdir=dist && tsc -p tsconfig.build.json",
24
+ "build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --outdir=dist && bun build src/index.ts --target=node --format=cjs --outfile=dist/index.cjs && tsc -p tsconfig.build.json && cp dist/index.d.ts dist/index.d.cts",
24
25
  "test": "bun test tests",
25
26
  "prepublishOnly": "bun run build && bun run test"
26
27
  }