@yuuko1410/feishu-bitable 0.0.2 → 0.0.4

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/lib/index.js CHANGED
@@ -96,7 +96,8 @@ function normalizeRecordFields(fields) {
96
96
  }
97
97
  function splitUpdateRecord(record) {
98
98
  const { record_id: recordId, ...fields } = record;
99
- return { recordId, fields };
99
+ const normalizedFields = Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== undefined));
100
+ return { recordId, fields: normalizedFields };
100
101
  }
101
102
  async function toBuffer(file) {
102
103
  if (typeof file === "string") {
@@ -149,18 +150,44 @@ function isFileLike(value) {
149
150
  }
150
151
 
151
152
  // src/client.ts
153
+ var DEFAULT_LOGGER = {
154
+ info(message, meta) {
155
+ if (meta) {
156
+ console.info(`[feishu-bitable] ${message}`, meta);
157
+ return;
158
+ }
159
+ console.info(`[feishu-bitable] ${message}`);
160
+ },
161
+ warn(message, meta) {
162
+ if (meta) {
163
+ console.warn(`[feishu-bitable] ${message}`, meta);
164
+ return;
165
+ }
166
+ console.warn(`[feishu-bitable] ${message}`);
167
+ },
168
+ error(message, meta) {
169
+ if (meta) {
170
+ console.error(`[feishu-bitable] ${message}`, meta);
171
+ return;
172
+ }
173
+ console.error(`[feishu-bitable] ${message}`);
174
+ }
175
+ };
176
+
152
177
  class Bitable {
153
178
  client;
154
179
  defaultAppToken;
155
180
  maxRetries;
156
181
  retryDelayMs;
157
182
  defaultConcurrency;
183
+ logger;
158
184
  constructor(optionsOrToken, appId, appSecret) {
159
185
  const options = this.resolveConstructorOptions(optionsOrToken, appId, appSecret);
160
186
  this.defaultAppToken = options.defaultAppToken;
161
187
  this.maxRetries = Math.max(1, options.maxRetries ?? 5);
162
188
  this.retryDelayMs = Math.max(100, options.retryDelayMs ?? 1000);
163
189
  this.defaultConcurrency = Math.max(1, options.defaultConcurrency ?? 1);
190
+ this.logger = options.logger === null ? null : options.logger ?? DEFAULT_LOGGER;
164
191
  this.client = options.sdkClient ?? new lark.Client({
165
192
  appId: options.appId,
166
193
  appSecret: options.appSecret,
@@ -176,159 +203,295 @@ class Bitable {
176
203
  });
177
204
  }
178
205
  async fetchAllRecords(tableId, options = {}, appToken) {
179
- const token = this.resolveAppToken(appToken);
180
- const pageSize = Math.max(1, Math.min(options.pageSize ?? FEISHU_BATCH_LIMIT, FEISHU_BATCH_LIMIT));
181
- const iterator = await this.client.bitable.v1.appTableRecord.searchWithIterator({
182
- path: {
183
- app_token: token,
184
- table_id: tableId
185
- },
186
- params: {
187
- page_size: pageSize,
188
- user_id_type: "open_id"
189
- },
190
- data: {
191
- view_id: options.viewId,
192
- field_names: options.fieldNames,
193
- filter: options.filter,
194
- sort: options.sort,
195
- automatic_fields: options.automaticFields
206
+ return this.runLogged("fetchAllRecords", {
207
+ tableId,
208
+ pageSize: options.pageSize ?? FEISHU_BATCH_LIMIT,
209
+ hasViewId: Boolean(options.viewId),
210
+ fieldCount: options.fieldNames?.length ?? 0
211
+ }, async () => {
212
+ const token = this.resolveAppToken(appToken);
213
+ const pageSize = Math.max(1, Math.min(options.pageSize ?? FEISHU_BATCH_LIMIT, FEISHU_BATCH_LIMIT));
214
+ const request = {
215
+ path: {
216
+ app_token: token,
217
+ table_id: tableId
218
+ },
219
+ params: {
220
+ page_size: pageSize,
221
+ user_id_type: "open_id"
222
+ },
223
+ data: {
224
+ view_id: options.viewId,
225
+ field_names: options.fieldNames,
226
+ filter: options.filter,
227
+ sort: options.sort,
228
+ automatic_fields: options.automaticFields
229
+ }
230
+ };
231
+ const iterator = await this.client.bitable.v1.appTableRecord.searchWithIterator(this.logRequest("fetchAllRecords request", request));
232
+ const allRecords = [];
233
+ for await (const page of iterator) {
234
+ const items = page?.items ?? [];
235
+ allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
196
236
  }
237
+ this.logInfo("fetchAllRecords completed", {
238
+ tableId,
239
+ recordCount: allRecords.length
240
+ });
241
+ return allRecords;
197
242
  });
198
- const allRecords = [];
199
- for await (const page of iterator) {
200
- const items = page?.items ?? [];
201
- allRecords.push(...items.map((item) => normalizeRecord(item.record_id, item.fields, options.normalizeFields !== false)));
202
- }
203
- return allRecords;
204
243
  }
205
244
  async insertList(tableId, records, options = {}) {
206
- if (records.length === 0) {
207
- return [];
208
- }
209
- const token = this.resolveAppToken(options.appToken);
210
- const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
211
- return runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("insert records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchCreate({
212
- path: { app_token: token, table_id: tableId },
213
- data: {
214
- records: chunk.map((fields) => ({ fields }))
245
+ return this.runLogged("insertList", {
246
+ tableId,
247
+ recordCount: records.length,
248
+ chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
249
+ concurrency: options.concurrency ?? this.defaultConcurrency
250
+ }, async () => {
251
+ if (records.length === 0) {
252
+ return [];
215
253
  }
216
- }), "insert records")));
254
+ const token = this.resolveAppToken(options.appToken);
255
+ const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
256
+ const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("insert records", async () => {
257
+ const request = {
258
+ path: { app_token: token, table_id: tableId },
259
+ data: {
260
+ records: chunk.map((fields) => ({ fields }))
261
+ }
262
+ };
263
+ return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchCreate(this.logRequest("insertList request", request)), "insert records");
264
+ }));
265
+ this.logInfo("insertList completed", {
266
+ tableId,
267
+ chunkCount: chunks.length
268
+ });
269
+ return responses;
270
+ });
217
271
  }
218
272
  async batchUpdateRecords(payload) {
219
- return this.withRetry("batch update records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchUpdate(payload), "batch update records"));
273
+ return this.runLogged("batchUpdateRecords", {
274
+ tableId: payload.path.table_id,
275
+ recordCount: payload.data.records.length
276
+ }, async () => this.executeBatchUpdateRecords(payload));
220
277
  }
221
278
  async updateRecords(tableId, records, options = {}) {
222
- if (records.length === 0) {
223
- return [];
224
- }
225
- const token = this.resolveAppToken(options.appToken);
226
- const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
227
- return runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => {
228
- const payload = {
229
- path: {
230
- app_token: token,
231
- table_id: tableId
232
- },
233
- data: {
234
- records: chunk.map((record) => {
235
- const { recordId, fields } = splitUpdateRecord(record);
236
- return {
237
- record_id: recordId,
238
- fields
239
- };
240
- })
279
+ return this.runLogged("updateRecords", {
280
+ tableId,
281
+ inputRecordCount: records.length,
282
+ chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
283
+ concurrency: options.concurrency ?? this.defaultConcurrency
284
+ }, async () => {
285
+ if (records.length === 0) {
286
+ return [];
287
+ }
288
+ const token = this.resolveAppToken(options.appToken);
289
+ const chunks = chunkArray(records, options.chunkSize ?? FEISHU_BATCH_LIMIT);
290
+ const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk, index) => {
291
+ const batchRecords = chunk.map((record, recordIndex) => {
292
+ const { recordId, fields } = splitUpdateRecord(record);
293
+ if (!recordId || !recordId.trim()) {
294
+ throw new FeishuBitableError(`updateRecords failed: record_id is required for chunk ${index}, item ${recordIndex}`, {
295
+ details: {
296
+ tableId,
297
+ chunkIndex: index,
298
+ recordIndex,
299
+ record
300
+ }
301
+ });
302
+ }
303
+ if (Object.keys(fields).length === 0) {
304
+ return null;
305
+ }
306
+ return {
307
+ record_id: recordId,
308
+ fields
309
+ };
310
+ }).filter((record) => Boolean(record));
311
+ if (batchRecords.length === 0) {
312
+ this.logInfo("updateRecords skipped empty chunk", {
313
+ tableId,
314
+ chunkIndex: index,
315
+ inputChunkSize: chunk.length
316
+ });
317
+ return {
318
+ code: 0,
319
+ msg: "ok",
320
+ data: {
321
+ records: []
322
+ }
323
+ };
241
324
  }
242
- };
243
- if (options.userIdType || options.ignoreConsistencyCheck !== undefined) {
244
- payload.params = {
245
- user_id_type: options.userIdType,
246
- ignore_consistency_check: options.ignoreConsistencyCheck
325
+ const payload = {
326
+ path: {
327
+ app_token: token,
328
+ table_id: tableId
329
+ },
330
+ data: {
331
+ records: batchRecords
332
+ }
247
333
  };
248
- }
249
- return this.batchUpdateRecords(payload);
334
+ if (options.userIdType || options.ignoreConsistencyCheck !== undefined) {
335
+ payload.params = {
336
+ user_id_type: options.userIdType,
337
+ ignore_consistency_check: options.ignoreConsistencyCheck
338
+ };
339
+ }
340
+ return this.executeBatchUpdateRecords(payload);
341
+ });
342
+ this.logInfo("updateRecords completed", {
343
+ tableId,
344
+ chunkCount: chunks.length
345
+ });
346
+ return responses;
250
347
  });
251
348
  }
252
349
  async deleteList(tableId, recordIds, options = {}) {
253
- if (recordIds.length === 0) {
254
- return [];
255
- }
256
- const token = this.resolveAppToken(options.appToken);
257
- const chunks = chunkArray(recordIds, options.chunkSize ?? FEISHU_BATCH_LIMIT);
258
- return runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("delete records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchDelete({
259
- path: { app_token: token, table_id: tableId },
260
- data: {
261
- records: chunk
350
+ return this.runLogged("deleteList", {
351
+ tableId,
352
+ recordCount: recordIds.length,
353
+ chunkSize: options.chunkSize ?? FEISHU_BATCH_LIMIT,
354
+ concurrency: options.concurrency ?? this.defaultConcurrency
355
+ }, async () => {
356
+ if (recordIds.length === 0) {
357
+ return [];
262
358
  }
263
- }), "delete records")));
359
+ const token = this.resolveAppToken(options.appToken);
360
+ const chunks = chunkArray(recordIds, options.chunkSize ?? FEISHU_BATCH_LIMIT);
361
+ const responses = await runWithConcurrency(chunks, options.concurrency ?? this.defaultConcurrency, async (chunk) => this.withRetry("delete records", async () => {
362
+ const request = {
363
+ path: { app_token: token, table_id: tableId },
364
+ data: {
365
+ records: chunk
366
+ }
367
+ };
368
+ return assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchDelete(this.logRequest("deleteList request", request)), "delete records");
369
+ }));
370
+ this.logInfo("deleteList completed", {
371
+ tableId,
372
+ chunkCount: chunks.length
373
+ });
374
+ return responses;
375
+ });
264
376
  }
265
377
  async uploadFile(options) {
266
- const buffer = await toBuffer(options.file);
267
- const fileName = options.fileName ?? inferFileName(options.file);
268
- if (buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT) {
269
- return await this.withRetry("upload file", async () => this.client.drive.v1.media.uploadAll({
270
- data: {
271
- file_name: fileName,
272
- parent_type: options.parentType,
273
- parent_node: options.parentNode,
274
- size: buffer.byteLength,
275
- extra: options.extra,
276
- file: buffer
277
- }
278
- })) ?? {};
279
- }
280
- const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => this.client.drive.v1.media.uploadPrepare({
281
- data: {
282
- file_name: fileName,
283
- parent_type: options.parentType,
284
- parent_node: options.parentNode,
285
- size: buffer.byteLength
286
- }
287
- })), "prepare multipart upload");
288
- const uploadId = prepare.data?.upload_id;
289
- const blockSize = prepare.data?.block_size;
290
- const blockNum = prepare.data?.block_num;
291
- if (!uploadId || !blockSize || !blockNum) {
292
- throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
293
- details: prepare
378
+ return this.runLogged("uploadFile", {
379
+ parentType: options.parentType,
380
+ parentNode: options.parentNode,
381
+ hasExtra: Boolean(options.extra)
382
+ }, async () => {
383
+ const buffer = await toBuffer(options.file);
384
+ const fileName = options.fileName ?? inferFileName(options.file);
385
+ this.logInfo("uploadFile resolved input", {
386
+ fileName,
387
+ size: buffer.byteLength,
388
+ mode: buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT ? "simple" : "multipart"
294
389
  });
295
- }
296
- for (let index = 0;index < blockNum; index++) {
297
- const start = index * blockSize;
298
- const end = Math.min(start + blockSize, buffer.byteLength);
299
- const chunk = buffer.subarray(start, end);
300
- await this.withRetry(`upload file chunk ${index + 1}/${blockNum}`, async () => this.client.drive.v1.media.uploadPart({
301
- data: {
302
- upload_id: uploadId,
303
- seq: index,
304
- size: chunk.byteLength,
305
- file: chunk
306
- }
307
- }));
308
- }
309
- const finish = assertFeishuResponse(await this.withRetry("finish multipart upload", async () => this.client.drive.v1.media.uploadFinish({
310
- data: {
311
- upload_id: uploadId,
312
- block_num: blockNum
390
+ if (buffer.byteLength <= FEISHU_SIMPLE_UPLOAD_LIMIT) {
391
+ return await this.withRetry("upload file", async () => {
392
+ const request = {
393
+ data: {
394
+ file_name: fileName,
395
+ parent_type: options.parentType,
396
+ parent_node: options.parentNode,
397
+ size: buffer.byteLength,
398
+ extra: options.extra,
399
+ file: buffer
400
+ }
401
+ };
402
+ this.logRequest("uploadFile request", {
403
+ data: {
404
+ ...request.data,
405
+ file: `[Buffer ${buffer.byteLength} bytes]`
406
+ }
407
+ });
408
+ return this.client.drive.v1.media.uploadAll(request);
409
+ }) ?? {};
313
410
  }
314
- })), "finish multipart upload");
315
- return {
316
- file_token: finish.data?.file_token
317
- };
411
+ const prepare = assertFeishuResponse(await this.withRetry("prepare multipart upload", async () => {
412
+ const request = {
413
+ data: {
414
+ file_name: fileName,
415
+ parent_type: options.parentType,
416
+ parent_node: options.parentNode,
417
+ size: buffer.byteLength
418
+ }
419
+ };
420
+ return this.client.drive.v1.media.uploadPrepare(this.logRequest("uploadPrepare request", request));
421
+ }), "prepare multipart upload");
422
+ const uploadId = prepare.data?.upload_id;
423
+ const blockSize = prepare.data?.block_size;
424
+ const blockNum = prepare.data?.block_num;
425
+ if (!uploadId || !blockSize || !blockNum) {
426
+ throw new FeishuBitableError("prepare multipart upload failed: missing upload metadata", {
427
+ details: prepare
428
+ });
429
+ }
430
+ for (let index = 0;index < blockNum; index++) {
431
+ const start = index * blockSize;
432
+ const end = Math.min(start + blockSize, buffer.byteLength);
433
+ const chunk = buffer.subarray(start, end);
434
+ await this.withRetry(`upload file chunk ${index + 1}/${blockNum}`, async () => {
435
+ const request = {
436
+ data: {
437
+ upload_id: uploadId,
438
+ seq: index,
439
+ size: chunk.byteLength,
440
+ file: chunk
441
+ }
442
+ };
443
+ this.logRequest("uploadPart request", {
444
+ data: {
445
+ ...request.data,
446
+ file: `[Buffer ${chunk.byteLength} bytes]`
447
+ }
448
+ });
449
+ return this.client.drive.v1.media.uploadPart(request);
450
+ });
451
+ }
452
+ const finish = assertFeishuResponse(await this.withRetry("finish multipart upload", async () => {
453
+ const request = {
454
+ data: {
455
+ upload_id: uploadId,
456
+ block_num: blockNum
457
+ }
458
+ };
459
+ return this.client.drive.v1.media.uploadFinish(this.logRequest("uploadFinish request", request));
460
+ }), "finish multipart upload");
461
+ return {
462
+ file_token: finish.data?.file_token
463
+ };
464
+ });
318
465
  }
319
466
  async downloadFile(fileToken, extra) {
320
- const response = await this.withRetry("download file", async () => this.client.drive.v1.media.download({
321
- path: {
322
- file_token: fileToken
323
- },
324
- params: {
325
- extra
326
- }
327
- }));
328
- return readableToBuffer(response.getReadableStream());
467
+ return this.runLogged("downloadFile", {
468
+ fileToken,
469
+ hasExtra: Boolean(extra)
470
+ }, async () => {
471
+ const response = await this.withRetry("download file", async () => {
472
+ const request = {
473
+ path: {
474
+ file_token: fileToken
475
+ },
476
+ params: {
477
+ extra
478
+ }
479
+ };
480
+ return this.client.drive.v1.media.download(this.logRequest("downloadFile request", request));
481
+ });
482
+ const buffer = await readableToBuffer(response.getReadableStream());
483
+ this.logInfo("downloadFile completed", {
484
+ fileToken,
485
+ size: buffer.byteLength
486
+ });
487
+ return buffer;
488
+ });
329
489
  }
330
490
  async downLoadFile(fileToken, extra) {
331
- return this.downloadFile(fileToken, extra);
491
+ return this.runLogged("downLoadFile", {
492
+ fileToken,
493
+ hasExtra: Boolean(extra)
494
+ }, async () => this.downloadFile(fileToken, extra));
332
495
  }
333
496
  resolveConstructorOptions(optionsOrToken, appIdArg, appSecretArg) {
334
497
  const objectMode = typeof optionsOrToken === "object" && optionsOrToken !== null && !Array.isArray(optionsOrToken);
@@ -356,6 +519,9 @@ class Bitable {
356
519
  }
357
520
  return token;
358
521
  }
522
+ async executeBatchUpdateRecords(payload) {
523
+ return this.withRetry("batch update records", async () => assertFeishuResponse(await this.client.bitable.v1.appTableRecord.batchUpdate(this.logRequest("batchUpdateRecords request", payload)), "batch update records"));
524
+ }
359
525
  async withRetry(label, task) {
360
526
  let lastError;
361
527
  for (let attempt = 1;attempt <= this.maxRetries; attempt++) {
@@ -363,15 +529,75 @@ class Bitable {
363
529
  return await task();
364
530
  } catch (error) {
365
531
  lastError = error;
532
+ const meta = {
533
+ label,
534
+ attempt,
535
+ maxRetries: this.maxRetries,
536
+ retryDelayMs: this.retryDelayMs,
537
+ ...this.getErrorMeta(error)
538
+ };
366
539
  if (attempt === this.maxRetries) {
540
+ this.logError("request exhausted retries", meta);
367
541
  break;
368
542
  }
543
+ this.logWarn("request attempt failed, retrying", meta);
369
544
  await sleep(this.retryDelayMs * attempt);
370
545
  }
371
546
  }
372
- throw new FeishuBitableError(`${label} failed after ${this.maxRetries} attempts`, {
373
- cause: lastError
547
+ const causeMessage = lastError instanceof Error ? `: ${lastError.message}` : "";
548
+ throw new FeishuBitableError(`${label} failed after ${this.maxRetries} attempts${causeMessage}`, {
549
+ cause: lastError,
550
+ code: lastError instanceof FeishuBitableError ? lastError.code : undefined,
551
+ details: lastError instanceof FeishuBitableError ? lastError.details : undefined
552
+ });
553
+ }
554
+ async runLogged(action, meta, task) {
555
+ this.logInfo(`${action} started`, meta);
556
+ try {
557
+ const result = await task();
558
+ this.logInfo(`${action} succeeded`, meta);
559
+ return result;
560
+ } catch (error) {
561
+ this.logError(`${action} failed`, {
562
+ ...meta,
563
+ ...this.getErrorMeta(error)
564
+ });
565
+ throw error;
566
+ }
567
+ }
568
+ logInfo(message, meta) {
569
+ this.logger?.info(message, meta);
570
+ }
571
+ logWarn(message, meta) {
572
+ this.logger?.warn?.(message, meta);
573
+ }
574
+ logError(message, meta) {
575
+ this.logger?.error?.(message, meta);
576
+ }
577
+ getErrorMeta(error) {
578
+ if (error instanceof FeishuBitableError) {
579
+ return {
580
+ errorName: error.name,
581
+ errorMessage: error.message,
582
+ errorCode: error.code,
583
+ errorDetails: error.details
584
+ };
585
+ }
586
+ if (error instanceof Error) {
587
+ return {
588
+ errorName: error.name,
589
+ errorMessage: error.message
590
+ };
591
+ }
592
+ return {
593
+ errorMessage: String(error)
594
+ };
595
+ }
596
+ logRequest(message, payload) {
597
+ this.logInfo(message, {
598
+ payload
374
599
  });
600
+ return payload;
375
601
  }
376
602
  }
377
603
 
package/lib/types.d.ts CHANGED
@@ -62,6 +62,11 @@ export type BitableFilterGroup = {
62
62
  };
63
63
  export type MediaParentType = "doc_image" | "docx_image" | "sheet_image" | "doc_file" | "docx_file" | "sheet_file" | "vc_virtual_background" | "bitable_image" | "bitable_file" | "moments" | "ccm_import_open" | "calendar" | "base_global" | "lark_ai_media_analysis";
64
64
  export type UploadableFile = string | Buffer | Uint8Array | ArrayBuffer | Blob | BunFileLike;
65
+ export type BitableLogger = {
66
+ info: (message: string, meta?: Record<string, unknown>) => void;
67
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
68
+ error?: (message: string, meta?: Record<string, unknown>) => void;
69
+ };
65
70
  export type BunFileLike = {
66
71
  arrayBuffer(): Promise<ArrayBuffer>;
67
72
  name?: string;
@@ -76,6 +81,7 @@ export interface BitableConstructorOptions {
76
81
  retryDelayMs?: number;
77
82
  defaultConcurrency?: number;
78
83
  sdkClient?: lark.Client;
84
+ logger?: BitableLogger | null;
79
85
  }
80
86
  export interface FetchAllRecordsOptions {
81
87
  viewId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuuko1410/feishu-bitable",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "基于 Bun + TypeScript + 飞书官方 SDK 的多维表格操作库",
5
5
  "type": "module",
6
6
  "main": "./lib/index.cjs",