@chriscdn/file-cache 0.0.3 → 0.0.5

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
@@ -61,9 +61,8 @@ const options: FileCacheOptions<MyCallbackArgs> = {
61
61
  await downloadAndConvertToJPG(url, filePath);
62
62
  },
63
63
 
64
- // Specifies the file extension for each cached file.
65
- // Currently, all files share the same extension (this may change in the future).
66
- ext: ".jpg",
64
+ // Specifies the file extension for the cached file. Can be synchronous or asynchronous.
65
+ ext: (context: MyCallbackArgs) => ".jpg",
67
66
 
68
67
  // Determines the time-to-live (TTL) of a cached file, in milliseconds,
69
68
  // based on when it was last accessed.
@@ -18,7 +18,7 @@ describe("Cache Expiration", async () => {
18
18
  filePath,
19
19
  );
20
20
  },
21
- ext: "pdf",
21
+ ext: () => ".pdf",
22
22
  ttl: Duration.toMilliseconds({ seconds: 1 }),
23
23
  cleanupInterval: Duration.toMilliseconds({ seconds: 2 }),
24
24
  });
package/lib/index.d.ts CHANGED
@@ -5,9 +5,9 @@ export type FileCacheOptions<T extends Record<string, any>> = {
5
5
  cachePath: DirectoryPath;
6
6
  autoCreateCachePath?: boolean;
7
7
  cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;
8
- ext: string;
9
- cleanupInterval?: Milliseconds;
8
+ ext: (context: T) => string | Promise<string>;
10
9
  ttl: Milliseconds;
10
+ cleanupInterval?: Milliseconds;
11
11
  };
12
12
  declare class FileCache<T extends Record<string, any>> {
13
13
  private _cachePath;
@@ -60,7 +60,6 @@ declare class FileCache<T extends Record<string, any>> {
60
60
  * @param args The arguments used to resolve the file path
61
61
  * @returns The full path to the cached file
62
62
  */
63
- resolveFilePath(args: T): FilePath;
64
- resolveCreateFilePath(args: T): Promise<FilePath>;
63
+ resolveFilePath(args: T): Promise<FilePath>;
65
64
  }
66
65
  export { type DirectoryPath, FileCache, type FilePath };
package/lib/index.js CHANGED
@@ -7,10 +7,10 @@ import touch from "touch";
7
7
  import { findNuke } from "@chriscdn/find-nuke";
8
8
  import { Duration } from "@chriscdn/duration";
9
9
  import { rimraf } from "rimraf";
10
+ import { Memoize } from "@chriscdn/memoize";
10
11
  const fsp = fs.promises;
11
12
  class FileCache {
12
13
  _cachePath;
13
- // private _resolveFileName: FileCacheOptions<T>["resolveFileName"];
14
14
  _cb;
15
15
  _ext;
16
16
  _ttl;
@@ -20,30 +20,25 @@ class FileCache {
20
20
  _cleanupSemaphore = new Semaphore();
21
21
  constructor({ cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval }) {
22
22
  this._cachePath = cachePath;
23
- // this._resolveFileName = resolveFileName;
24
23
  this._cb = cb;
25
24
  this._ext = ext;
26
25
  this._ttl = ttl;
27
26
  this._cleanupInterval = cleanupInterval ??
28
27
  Duration.toMilliseconds({ days: 1 });
29
- (async () => {
30
- if (!(await pathExists(cachePath))) {
31
- throw new Error("💥 FileCache error: cachePath does not exist.");
32
- // process.exit(1); // or throw new Error() if you want to let it bubble
33
- }
34
- })();
35
28
  if ((pathExistsSync(cachePath))) {
29
+ // we're good
36
30
  }
37
31
  else if (autoCreateCachePath) {
38
32
  fs.mkdirSync(cachePath, { recursive: true });
39
33
  }
40
34
  else {
41
35
  throw new Error("💥 FileCache error: cachePath does not exist.");
42
- // process.exit(1); // or throw new Error() if you want to let it bubble
43
36
  }
44
37
  // fire and forget
45
38
  this.cleanup();
46
39
  this._intervalId = setInterval(this.cleanup.bind(this), this._cleanupInterval);
40
+ this.resolveFileName = Memoize(this.resolveFileName.bind(this));
41
+ this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
47
42
  }
48
43
  /**
49
44
  * Runs a cleanup pass to remove files older than 1 day from the cache.
@@ -73,18 +68,18 @@ class FileCache {
73
68
  * @returns The path to the cached or newly created file
74
69
  */
75
70
  async getFile(args) {
76
- const filePath = this.resolveFilePath(args);
71
+ const filePath = await this.resolveFilePath(args);
77
72
  try {
78
73
  await Promise.all([
79
74
  this._semaphore.acquire(filePath),
80
- this._cleanupSemaphore.wait(),
75
+ this._cleanupSemaphore.wait(), // wait for any cleanup task to end
81
76
  ]);
82
77
  if (await pathExists(filePath)) {
83
78
  // fire and forget
84
79
  touch(filePath);
85
80
  }
86
81
  else {
87
- await fs.mkdirSync(path.dirname(filePath), { recursive: true });
82
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
88
83
  await this._cb(filePath, args, this);
89
84
  }
90
85
  }
@@ -94,7 +89,7 @@ class FileCache {
94
89
  return filePath;
95
90
  }
96
91
  async has(args) {
97
- const filePath = this.resolveFilePath(args);
92
+ const filePath = await this.resolveFilePath(args);
98
93
  return await pathExists(filePath);
99
94
  }
100
95
  /**
@@ -104,13 +99,19 @@ class FileCache {
104
99
  * @returns True if the file was deleted, false if it didn't exist
105
100
  */
106
101
  async expire(args) {
107
- const filePath = this.resolveFilePath(args);
108
- if (await pathExists(filePath)) {
109
- await rimraf(filePath);
110
- return true;
102
+ const filePath = await this.resolveFilePath(args);
103
+ try {
104
+ await this._semaphore.acquire(filePath);
105
+ if (await pathExists(filePath)) {
106
+ await rimraf(filePath);
107
+ return true;
108
+ }
109
+ else {
110
+ return false;
111
+ }
111
112
  }
112
- else {
113
- return false;
113
+ finally {
114
+ this._semaphore.release(filePath);
114
115
  }
115
116
  }
116
117
  /**
@@ -128,16 +129,12 @@ class FileCache {
128
129
  * Uses the provided `resolveFileName` function if available, or falls back to
129
130
  * generating a hash from the arguments to create a unique file name.
130
131
  */
131
- resolveFileName(args) {
132
+ async resolveFileName(args) {
132
133
  const baseName = sha1(JSON.stringify(args));
133
134
  return path.format({
134
- // dir: path.dirname(filePath),
135
135
  name: baseName,
136
- ext: this._ext,
136
+ ext: await this._ext(args),
137
137
  });
138
- // return this._resolveFileName
139
- // ? this._resolveFileName(args)
140
- // : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;
141
138
  }
142
139
  /**
143
140
  * Resolves the full file path for the cached file based on the provided arguments.
@@ -145,17 +142,13 @@ class FileCache {
145
142
  * @param args The arguments used to resolve the file path
146
143
  * @returns The full path to the cached file
147
144
  */
148
- resolveFilePath(args) {
149
- const fileName = this.resolveFileName(args);
145
+ async resolveFilePath(args) {
146
+ const fileName = await this.resolveFileName(args);
150
147
  const filePath = path.resolve(this._cachePath, fileName[0], fileName[1], fileName[2],
151
148
  // fileName[3],
152
149
  fileName);
153
150
  return filePath;
154
151
  }
155
- async resolveCreateFilePath(args) {
156
- const filePath = this.resolveFilePath(args);
157
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
158
- return filePath;
159
- }
160
152
  }
161
153
  export { FileCache };
154
+ //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1,7 +1 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import path from \"path\";\nimport fs from \"fs\";\nimport { pathExists, pathExistsSync } from \"path-exists\";\nimport sha1 from \"sha1\";\nimport Semaphore from \"@chriscdn/promise-semaphore\";\nimport touch from \"touch\";\nimport { findNuke } from \"@chriscdn/find-nuke\";\nimport { Duration } from \"@chriscdn/duration\";\nimport { rimraf } from \"rimraf\";\n\nconst fsp = fs.promises;\n\ntype DirectoryPath = string;\ntype FilePath = string;\ntype Milliseconds = number;\n\nexport type FileCacheOptions<T extends Record<string, any>> = {\n cachePath: DirectoryPath;\n autoCreateCachePath?: boolean;\n cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;\n ext: string;\n cleanupInterval?: Milliseconds;\n ttl: Milliseconds;\n // resolveFileName?: (args: T) => FilePath;\n};\n\nclass FileCache<T extends Record<string, any>> {\n private _cachePath: FileCacheOptions<T>[\"cachePath\"];\n // private _resolveFileName: FileCacheOptions<T>[\"resolveFileName\"];\n private _cb: FileCacheOptions<T>[\"cb\"];\n private _ext: FileCacheOptions<T>[\"ext\"];\n private _ttl: FileCacheOptions<T>[\"ttl\"];\n private _cleanupInterval: FileCacheOptions<T>[\"cleanupInterval\"];\n\n private _intervalId: ReturnType<typeof setInterval> | null = null;\n\n private _semaphore: Semaphore = new Semaphore();\n private _cleanupSemaphore: Semaphore = new Semaphore();\n\n constructor(\n { cachePath, autoCreateCachePath, cb, ext, ttl, cleanupInterval }:\n FileCacheOptions<T>,\n ) {\n this._cachePath = cachePath;\n // this._resolveFileName = resolveFileName;\n this._cb = cb;\n this._ext = ext;\n this._ttl = ttl;\n this._cleanupInterval = cleanupInterval ??\n Duration.toMilliseconds({ days: 1 });\n\n (async () => {\n if (!(await pathExists(cachePath))) {\n throw new Error(\"\uD83D\uDCA5 FileCache error: cachePath does not exist.\");\n // process.exit(1); // or throw new Error() if you want to let it bubble\n }\n })();\n\n if ((pathExistsSync(cachePath))) {\n } else if (autoCreateCachePath) {\n fs.mkdirSync(cachePath, { recursive: true });\n } else {\n throw new Error(\"\uD83D\uDCA5 FileCache error: cachePath does not exist.\");\n // process.exit(1); // or throw new Error() if you want to let it bubble\n }\n\n // fire and forget\n this.cleanup();\n\n this._intervalId = setInterval(\n this.cleanup.bind(this),\n this._cleanupInterval,\n );\n }\n\n /**\n * Runs a cleanup pass to remove files older than 1 day from the cache.\n * This is called periodically, but can be triggered manually.\n */\n async cleanup() {\n try {\n await this._cleanupSemaphore.acquire();\n await findNuke(this._cachePath, {\n olderThan: this._ttl,\n verbose: true,\n deleteEmptyDirectories: true,\n });\n } finally {\n this._cleanupSemaphore.release();\n }\n }\n\n /**\n * Retrieves a cached file for the given arguments.\n * If the file doesn't exist, it is generated by the callback and stored.\n *\n * Concurrency-safe, ensuring no conflicts when multiple requests are made\n * for the same file.\n *\n * @param args The arguments used to determine the file's identity and generation\n * @returns The path to the cached or newly created file\n */\n async getFile(args: T): Promise<FilePath> {\n const filePath = this.resolveFilePath(args);\n\n try {\n await Promise.all([\n this._semaphore.acquire(filePath),\n this._cleanupSemaphore.wait(),\n ]);\n\n if (await pathExists(filePath)) {\n // fire and forget\n touch(filePath);\n } else {\n await fs.mkdirSync(path.dirname(filePath), { recursive: true });\n await this._cb(filePath, args, this);\n }\n } finally {\n this._semaphore.release(filePath);\n }\n\n return filePath;\n }\n\n async has(args: T): Promise<boolean> {\n const filePath = this.resolveFilePath(args);\n return await pathExists(filePath);\n }\n\n /**\n * Deletes a file from the cache if it exists.\n *\n * @param args The arguments used to determine the file's identity\n * @returns True if the file was deleted, false if it didn't exist\n */\n async expire(args: T): Promise<boolean> {\n const filePath = this.resolveFilePath(args);\n\n if (await pathExists(filePath)) {\n await rimraf(filePath);\n return true;\n } else {\n return false;\n }\n }\n\n /**\n * Stops the background cleanup process, but does not delete any cached files.\n */\n destroy() {\n if (this._intervalId) {\n clearInterval(this._intervalId);\n this._intervalId = null;\n }\n }\n\n /**\n * Resolves the filename for the cache based on the provided arguments.\n *\n * Uses the provided `resolveFileName` function if available, or falls back to\n * generating a hash from the arguments to create a unique file name.\n */\n private resolveFileName(args: T): string {\n const baseName = sha1(JSON.stringify(args));\n\n return path.format({\n // dir: path.dirname(filePath),\n name: baseName,\n ext: this._ext,\n });\n\n // return this._resolveFileName\n // ? this._resolveFileName(args)\n // : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;\n }\n\n /**\n * Resolves the full file path for the cached file based on the provided arguments.\n *\n * @param args The arguments used to resolve the file path\n * @returns The full path to the cached file\n */\n resolveFilePath(args: T): FilePath {\n const fileName = this.resolveFileName(args);\n\n const filePath = path.resolve(\n this._cachePath,\n fileName[0],\n fileName[1],\n fileName[2],\n // fileName[3],\n fileName,\n );\n\n return filePath;\n }\n\n async resolveCreateFilePath(args: T): Promise<FilePath> {\n const filePath = this.resolveFilePath(args);\n\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n\n return filePath;\n }\n}\n\nexport { type DirectoryPath, FileCache, type FilePath };\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscdn/file-cache",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "File cache ",
5
5
  "repository": "https://github.com/chriscdn/file-cache",
6
6
  "author": "Christopher Meyer <chris@schwiiz.org>",
@@ -10,18 +10,17 @@
10
10
  "module": "./lib/index.js",
11
11
  "types": "./lib/index.d.ts",
12
12
  "exports": {
13
- "types": "./lib/index.d.ts",
14
13
  "default": "./lib/index.js"
15
14
  },
16
15
  "scripts": {
17
- "build": "rimraf ./lib/ && node ./esbuild.mjs && tsc --declaration",
18
- "dev": "microbundle watch",
16
+ "build": "rimraf ./lib/ && tsc",
19
17
  "test": "vitest"
20
18
  },
21
19
  "dependencies": {
22
20
  "@chriscdn/duration": "^1.0.3",
23
21
  "@chriscdn/find-nuke": "^0.0.4",
24
- "@chriscdn/promise-semaphore": "^2.0.11",
22
+ "@chriscdn/memoize": "^1.0.9",
23
+ "@chriscdn/promise-semaphore": "^2.0.12",
25
24
  "path-exists": "^5.0.0",
26
25
  "rimraf": "^6.0.1",
27
26
  "sha1": "^1.1.1",
@@ -29,9 +28,9 @@
29
28
  "touch": "^3.1.1"
30
29
  },
31
30
  "devDependencies": {
31
+ "@tsconfig/node22": "^22.0.1",
32
32
  "@types/sha1": "^1.1.5",
33
33
  "@types/touch": "^3.1.5",
34
- "esbuild": "^0.25.3",
35
34
  "typescript": "^5.8.3",
36
35
  "vitest": "^3.1.2"
37
36
  }
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import touch from "touch";
7
7
  import { findNuke } from "@chriscdn/find-nuke";
8
8
  import { Duration } from "@chriscdn/duration";
9
9
  import { rimraf } from "rimraf";
10
-
10
+ import { Memoize } from "@chriscdn/memoize";
11
11
  const fsp = fs.promises;
12
12
 
13
13
  type DirectoryPath = string;
@@ -18,15 +18,14 @@ export type FileCacheOptions<T extends Record<string, any>> = {
18
18
  cachePath: DirectoryPath;
19
19
  autoCreateCachePath?: boolean;
20
20
  cb: (filePath: FilePath, context: T, cache: FileCache<T>) => Promise<void>;
21
- ext: string;
22
- cleanupInterval?: Milliseconds;
21
+ ext: (context: T) => string | Promise<string>;
23
22
  ttl: Milliseconds;
24
- // resolveFileName?: (args: T) => FilePath;
23
+ cleanupInterval?: Milliseconds;
25
24
  };
26
25
 
27
26
  class FileCache<T extends Record<string, any>> {
28
27
  private _cachePath: FileCacheOptions<T>["cachePath"];
29
- // private _resolveFileName: FileCacheOptions<T>["resolveFileName"];
28
+
30
29
  private _cb: FileCacheOptions<T>["cb"];
31
30
  private _ext: FileCacheOptions<T>["ext"];
32
31
  private _ttl: FileCacheOptions<T>["ttl"];
@@ -42,26 +41,18 @@ class FileCache<T extends Record<string, any>> {
42
41
  FileCacheOptions<T>,
43
42
  ) {
44
43
  this._cachePath = cachePath;
45
- // this._resolveFileName = resolveFileName;
46
44
  this._cb = cb;
47
45
  this._ext = ext;
48
46
  this._ttl = ttl;
49
47
  this._cleanupInterval = cleanupInterval ??
50
48
  Duration.toMilliseconds({ days: 1 });
51
49
 
52
- (async () => {
53
- if (!(await pathExists(cachePath))) {
54
- throw new Error("💥 FileCache error: cachePath does not exist.");
55
- // process.exit(1); // or throw new Error() if you want to let it bubble
56
- }
57
- })();
58
-
59
50
  if ((pathExistsSync(cachePath))) {
51
+ // we're good
60
52
  } else if (autoCreateCachePath) {
61
53
  fs.mkdirSync(cachePath, { recursive: true });
62
54
  } else {
63
55
  throw new Error("💥 FileCache error: cachePath does not exist.");
64
- // process.exit(1); // or throw new Error() if you want to let it bubble
65
56
  }
66
57
 
67
58
  // fire and forget
@@ -71,6 +62,9 @@ class FileCache<T extends Record<string, any>> {
71
62
  this.cleanup.bind(this),
72
63
  this._cleanupInterval,
73
64
  );
65
+
66
+ this.resolveFileName = Memoize(this.resolveFileName.bind(this));
67
+ this.resolveFilePath = Memoize(this.resolveFilePath.bind(this));
74
68
  }
75
69
 
76
70
  /**
@@ -101,19 +95,19 @@ class FileCache<T extends Record<string, any>> {
101
95
  * @returns The path to the cached or newly created file
102
96
  */
103
97
  async getFile(args: T): Promise<FilePath> {
104
- const filePath = this.resolveFilePath(args);
98
+ const filePath = await this.resolveFilePath(args);
105
99
 
106
100
  try {
107
101
  await Promise.all([
108
102
  this._semaphore.acquire(filePath),
109
- this._cleanupSemaphore.wait(),
103
+ this._cleanupSemaphore.wait(), // wait for any cleanup task to end
110
104
  ]);
111
105
 
112
106
  if (await pathExists(filePath)) {
113
107
  // fire and forget
114
108
  touch(filePath);
115
109
  } else {
116
- await fs.mkdirSync(path.dirname(filePath), { recursive: true });
110
+ await fsp.mkdir(path.dirname(filePath), { recursive: true });
117
111
  await this._cb(filePath, args, this);
118
112
  }
119
113
  } finally {
@@ -124,7 +118,7 @@ class FileCache<T extends Record<string, any>> {
124
118
  }
125
119
 
126
120
  async has(args: T): Promise<boolean> {
127
- const filePath = this.resolveFilePath(args);
121
+ const filePath = await this.resolveFilePath(args);
128
122
  return await pathExists(filePath);
129
123
  }
130
124
 
@@ -135,13 +129,19 @@ class FileCache<T extends Record<string, any>> {
135
129
  * @returns True if the file was deleted, false if it didn't exist
136
130
  */
137
131
  async expire(args: T): Promise<boolean> {
138
- const filePath = this.resolveFilePath(args);
132
+ const filePath = await this.resolveFilePath(args);
139
133
 
140
- if (await pathExists(filePath)) {
141
- await rimraf(filePath);
142
- return true;
143
- } else {
144
- return false;
134
+ try {
135
+ await this._semaphore.acquire(filePath);
136
+
137
+ if (await pathExists(filePath)) {
138
+ await rimraf(filePath);
139
+ return true;
140
+ } else {
141
+ return false;
142
+ }
143
+ } finally {
144
+ this._semaphore.release(filePath);
145
145
  }
146
146
  }
147
147
 
@@ -161,18 +161,13 @@ class FileCache<T extends Record<string, any>> {
161
161
  * Uses the provided `resolveFileName` function if available, or falls back to
162
162
  * generating a hash from the arguments to create a unique file name.
163
163
  */
164
- private resolveFileName(args: T): string {
164
+ private async resolveFileName(args: T): Promise<string> {
165
165
  const baseName = sha1(JSON.stringify(args));
166
166
 
167
167
  return path.format({
168
- // dir: path.dirname(filePath),
169
168
  name: baseName,
170
- ext: this._ext,
169
+ ext: await this._ext(args),
171
170
  });
172
-
173
- // return this._resolveFileName
174
- // ? this._resolveFileName(args)
175
- // : `${sha1(JSON.stringify(args, Object.keys(args).sort()))}${this._ext}`;
176
171
  }
177
172
 
178
173
  /**
@@ -181,8 +176,8 @@ class FileCache<T extends Record<string, any>> {
181
176
  * @param args The arguments used to resolve the file path
182
177
  * @returns The full path to the cached file
183
178
  */
184
- resolveFilePath(args: T): FilePath {
185
- const fileName = this.resolveFileName(args);
179
+ async resolveFilePath(args: T): Promise<FilePath> {
180
+ const fileName = await this.resolveFileName(args);
186
181
 
187
182
  const filePath = path.resolve(
188
183
  this._cachePath,
@@ -196,13 +191,13 @@ class FileCache<T extends Record<string, any>> {
196
191
  return filePath;
197
192
  }
198
193
 
199
- async resolveCreateFilePath(args: T): Promise<FilePath> {
200
- const filePath = this.resolveFilePath(args);
194
+ // async resolveCreateFilePath(args: T): Promise<FilePath> {
195
+ // const filePath = this.resolveFilePath(args);
201
196
 
202
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
197
+ // await fsp.mkdir(path.dirname(filePath), { recursive: true });
203
198
 
204
- return filePath;
205
- }
199
+ // return filePath;
200
+ // }
206
201
  }
207
202
 
208
203
  export { type DirectoryPath, FileCache, type FilePath };
package/tsconfig.json CHANGED
@@ -1,15 +1,11 @@
1
1
  {
2
+ "extends": "@tsconfig/node22/tsconfig.json",
2
3
  "compilerOptions": {
3
- "target": "ESNext",
4
- "lib": ["ESNext"],
5
- "module": "ESNext",
6
- "moduleResolution": "node",
7
- "strict": true,
8
4
  "allowSyntheticDefaultImports": true,
9
5
  "outDir": "./lib",
10
- "declaration": true, // Generate declaration files
6
+ "declaration": true,
11
7
  "declarationDir": "./lib",
12
- "skipLibCheck": true
8
+ "sourceMap": true
13
9
  },
14
10
  "include": ["./src/**/*"]
15
11
  }
package/esbuild.mjs DELETED
@@ -1,19 +0,0 @@
1
- import { build } from "esbuild";
2
-
3
- import pkg from "./package.json" with { type: "json" };
4
-
5
- const external = [
6
- ...Object.keys(pkg.dependencies || {}),
7
- ...Object.keys(pkg.devDependencies || {}),
8
- ];
9
-
10
- build({
11
- entryPoints: ["./src/index.ts"],
12
- bundle: true,
13
- platform: "node",
14
- format: "esm", // or 'esm' if you prefer
15
- outfile: "./lib/index.js",
16
- external,
17
- minify: false,
18
- sourcemap: true,
19
- }).catch(() => process.exit(1));