@gringow/gringow 0.6.1 → 0.6.2

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.
Files changed (44) hide show
  1. package/dist/browser.d.mts +1 -1
  2. package/dist/browser.mjs +22 -22
  3. package/dist/config.d.mts +30 -3
  4. package/dist/config.mjs +12 -22
  5. package/dist/constants/default-config.mjs +2 -2
  6. package/dist/create-cache-id.d.mts +2 -2
  7. package/dist/create-cache-id.mjs +22 -21
  8. package/dist/data/cache-file-path.d.mts +13 -0
  9. package/dist/data/cache-file-path.mjs +6 -0
  10. package/dist/data/cache-queue.d.mts +35 -0
  11. package/dist/data/cache-queue.mjs +372 -0
  12. package/dist/data/check-cache.d.mts +17 -8
  13. package/dist/data/check-cache.mjs +53 -87
  14. package/dist/data/clear-cache.mjs +32 -84
  15. package/dist/data/create-cache-dir.d.mts +3 -0
  16. package/dist/data/create-cache-dir.mjs +11 -71
  17. package/dist/data/delete-cache-item.d.mts +11 -8
  18. package/dist/data/delete-cache-item.mjs +51 -103
  19. package/dist/data/get-cache.d.mts +2 -1
  20. package/dist/data/get-cache.mjs +17 -72
  21. package/dist/data/get-or-create-cache.d.mts +16 -8
  22. package/dist/data/get-or-create-cache.mjs +54 -88
  23. package/dist/data/md5.d.mts +2 -5
  24. package/dist/data/md5.mjs +10 -10
  25. package/dist/data/save-cache.d.mts +2 -9
  26. package/dist/data/save-cache.mjs +32 -84
  27. package/dist/data/write-cache-to-disk.d.mts +12 -8
  28. package/dist/data/write-cache-to-disk.mjs +49 -100
  29. package/dist/gringow.d.mts +19 -8
  30. package/dist/gringow.mjs +116 -91
  31. package/dist/index.d.mts +6 -78
  32. package/dist/index.mjs +130 -99
  33. package/dist/interpolate-template.d.mts +7 -0
  34. package/dist/interpolate-template.mjs +11 -11
  35. package/dist/io/ask-llm.d.mts +1 -8
  36. package/dist/io/ask-llm.mjs +32 -78
  37. package/dist/logger.d.mts +18 -0
  38. package/dist/logger.mjs +18 -0
  39. package/dist/schemas/config.shcema.d.mts +5 -1
  40. package/dist/schemas/config.shcema.mjs +3 -3
  41. package/dist/strip-comments.d.mts +1 -9
  42. package/dist/strip-comments.mjs +0 -1
  43. package/dist/types/index.d.mts +22 -1
  44. package/package.json +7 -8
@@ -0,0 +1,372 @@
1
+ import fs from 'fs/promises';
2
+ import { cosmiconfig } from 'cosmiconfig';
3
+ import mergeWith from 'lodash.mergewith';
4
+ import { z } from 'zod';
5
+ import { dirname, resolve, parse } from 'path';
6
+ import { mkdir, stat, statSync, mkdirSync } from 'fs';
7
+
8
+ // src/data/cache-queue.ts
9
+
10
+ // src/constants/default-config.ts
11
+ var defaultConfig = {
12
+ debug: false,
13
+ log: {
14
+ level: "debug"
15
+ },
16
+ cache: {
17
+ dir: "./gringow",
18
+ file: "gringow.json",
19
+ prefix: "grw",
20
+ write: true
21
+ },
22
+ sourceLanguage: "en-US",
23
+ defaultLanguage: "pt-BR",
24
+ languages: ["pt-BR", "fr-CA"],
25
+ llm: {
26
+ provider: "ollama",
27
+ apiKey: void 0,
28
+ model: "llama3.2",
29
+ temperature: 1,
30
+ sleep: 0
31
+ },
32
+ globby: "./src/**/*.{js,jsx,ts,tsx}"
33
+ };
34
+ var languageSchema = z.string().regex(/^[a-z]{2,3}(-[A-Z]{2})?$/);
35
+ var llmProviderSchema = z.enum([
36
+ "anthropic",
37
+ "gemini",
38
+ "openai",
39
+ "groq",
40
+ "mistral",
41
+ "ollama",
42
+ "xai",
43
+ "deepseek",
44
+ "openrouter",
45
+ "cerebras",
46
+ "lmstudio",
47
+ "azure",
48
+ "meta"
49
+ ]);
50
+ var keylessProviders = ["ollama", "lmstudio"];
51
+ var configSchema = z.object({
52
+ debug: z.boolean().optional(),
53
+ log: z.object({
54
+ level: z.enum(["debug", "info", "warn", "error", "trace"])
55
+ }),
56
+ cache: z.object({
57
+ dir: z.string(),
58
+ file: z.string(),
59
+ prefix: z.string(),
60
+ write: z.boolean().optional()
61
+ }),
62
+ sourceLanguage: languageSchema,
63
+ defaultLanguage: languageSchema,
64
+ languages: z.array(languageSchema),
65
+ llm: z.object({
66
+ provider: llmProviderSchema,
67
+ apiKey: z.string().optional(),
68
+ baseURL: z.string().optional(),
69
+ model: z.string().nullable(),
70
+ temperature: z.number().optional(),
71
+ sleep: z.number().optional(),
72
+ prePrompt: z.string().max(128).optional(),
73
+ postPrompt: z.string().max(128).optional(),
74
+ prePromptReplace: z.string().optional(),
75
+ postPromptReplace: z.string().optional(),
76
+ prePromptAppend: z.string().optional(),
77
+ postPromptPrepend: z.string().optional()
78
+ }).strict(),
79
+ globby: z.string().regex(/^\.\/.*/).optional()
80
+ }).refine(
81
+ (data) => {
82
+ if (keylessProviders.includes(data.llm.provider)) {
83
+ return true;
84
+ }
85
+ return !!data.llm.apiKey;
86
+ },
87
+ {
88
+ message: "The API key must be present in llm.apiKey for the chosen provider.",
89
+ path: ["llm", "apiKey"]
90
+ }
91
+ );
92
+
93
+ // src/config.ts
94
+ var explorer = cosmiconfig("gringow");
95
+ var result = await explorer.search();
96
+ function replaceArrays(_destination, source) {
97
+ if (Array.isArray(source)) return [...source];
98
+ return void 0;
99
+ }
100
+ async function loadUserConfig() {
101
+ try {
102
+ const mergedConfig = mergeWith({}, defaultConfig, result?.config, replaceArrays);
103
+ return await configSchema.parseAsync(mergedConfig);
104
+ } catch (error) {
105
+ console.warn(`${error?.message}, using default configuration`);
106
+ return defaultConfig;
107
+ }
108
+ }
109
+ var awaitedConfig = loadUserConfig();
110
+
111
+ // src/data/cache-file-path.ts
112
+ function cacheFilePath(config) {
113
+ return `${config.cache.dir}/${config.cache.file}`;
114
+ }
115
+ var optsArg = (opts) => {
116
+ if (!opts) {
117
+ opts = { mode: 511 };
118
+ } else if (typeof opts === "object") {
119
+ opts = { mode: 511, ...opts };
120
+ } else if (typeof opts === "number") {
121
+ opts = { mode: opts };
122
+ } else if (typeof opts === "string") {
123
+ opts = { mode: parseInt(opts, 8) };
124
+ } else {
125
+ throw new TypeError("invalid options argument");
126
+ }
127
+ const resolved = opts;
128
+ const optsFs = opts.fs || {};
129
+ opts.mkdir = opts.mkdir || optsFs.mkdir || mkdir;
130
+ opts.mkdirAsync = opts.mkdirAsync ? opts.mkdirAsync : async (path, options) => {
131
+ return new Promise((res, rej) => resolved.mkdir(path, options, (er, made) => er ? rej(er) : res(made)));
132
+ };
133
+ opts.stat = opts.stat || optsFs.stat || stat;
134
+ opts.statAsync = opts.statAsync ? opts.statAsync : async (path) => new Promise((res, rej) => resolved.stat(path, (err, stats) => err ? rej(err) : res(stats)));
135
+ opts.statSync = opts.statSync || optsFs.statSync || statSync;
136
+ opts.mkdirSync = opts.mkdirSync || optsFs.mkdirSync || mkdirSync;
137
+ return resolved;
138
+ };
139
+
140
+ // ../node_modules/.pnpm/mkdirp@3.0.1/node_modules/mkdirp/dist/mjs/mkdirp-manual.js
141
+ var mkdirpManualSync = (path, options, made) => {
142
+ const parent = dirname(path);
143
+ const opts = { ...optsArg(options), recursive: false };
144
+ if (parent === path) {
145
+ try {
146
+ return opts.mkdirSync(path, opts);
147
+ } catch (er) {
148
+ const fer = er;
149
+ if (fer && fer.code !== "EISDIR") {
150
+ throw er;
151
+ }
152
+ return;
153
+ }
154
+ }
155
+ try {
156
+ opts.mkdirSync(path, opts);
157
+ return made || path;
158
+ } catch (er) {
159
+ const fer = er;
160
+ if (fer && fer.code === "ENOENT") {
161
+ return mkdirpManualSync(path, opts, mkdirpManualSync(parent, opts, made));
162
+ }
163
+ if (fer && fer.code !== "EEXIST" && fer && fer.code !== "EROFS") {
164
+ throw er;
165
+ }
166
+ try {
167
+ if (!opts.statSync(path).isDirectory())
168
+ throw er;
169
+ } catch (_) {
170
+ throw er;
171
+ }
172
+ }
173
+ };
174
+ var mkdirpManual = Object.assign(async (path, options, made) => {
175
+ const opts = optsArg(options);
176
+ opts.recursive = false;
177
+ const parent = dirname(path);
178
+ if (parent === path) {
179
+ return opts.mkdirAsync(path, opts).catch((er) => {
180
+ const fer = er;
181
+ if (fer && fer.code !== "EISDIR") {
182
+ throw er;
183
+ }
184
+ });
185
+ }
186
+ return opts.mkdirAsync(path, opts).then(() => made || path, async (er) => {
187
+ const fer = er;
188
+ if (fer && fer.code === "ENOENT") {
189
+ return mkdirpManual(parent, opts).then((made2) => mkdirpManual(path, opts, made2));
190
+ }
191
+ if (fer && fer.code !== "EEXIST" && fer.code !== "EROFS") {
192
+ throw er;
193
+ }
194
+ return opts.statAsync(path).then((st) => {
195
+ if (st.isDirectory()) {
196
+ return made;
197
+ } else {
198
+ throw er;
199
+ }
200
+ }, () => {
201
+ throw er;
202
+ });
203
+ });
204
+ }, { sync: mkdirpManualSync });
205
+ var findMade = async (opts, parent, path) => {
206
+ if (path === parent) {
207
+ return;
208
+ }
209
+ return opts.statAsync(parent).then(
210
+ (st) => st.isDirectory() ? path : void 0,
211
+ // will fail later
212
+ // will fail later
213
+ (er) => {
214
+ const fer = er;
215
+ return fer && fer.code === "ENOENT" ? findMade(opts, dirname(parent), parent) : void 0;
216
+ }
217
+ );
218
+ };
219
+ var findMadeSync = (opts, parent, path) => {
220
+ if (path === parent) {
221
+ return void 0;
222
+ }
223
+ try {
224
+ return opts.statSync(parent).isDirectory() ? path : void 0;
225
+ } catch (er) {
226
+ const fer = er;
227
+ return fer && fer.code === "ENOENT" ? findMadeSync(opts, dirname(parent), parent) : void 0;
228
+ }
229
+ };
230
+
231
+ // ../node_modules/.pnpm/mkdirp@3.0.1/node_modules/mkdirp/dist/mjs/mkdirp-native.js
232
+ var mkdirpNativeSync = (path, options) => {
233
+ const opts = optsArg(options);
234
+ opts.recursive = true;
235
+ const parent = dirname(path);
236
+ if (parent === path) {
237
+ return opts.mkdirSync(path, opts);
238
+ }
239
+ const made = findMadeSync(opts, path);
240
+ try {
241
+ opts.mkdirSync(path, opts);
242
+ return made;
243
+ } catch (er) {
244
+ const fer = er;
245
+ if (fer && fer.code === "ENOENT") {
246
+ return mkdirpManualSync(path, opts);
247
+ } else {
248
+ throw er;
249
+ }
250
+ }
251
+ };
252
+ var mkdirpNative = Object.assign(async (path, options) => {
253
+ const opts = { ...optsArg(options), recursive: true };
254
+ const parent = dirname(path);
255
+ if (parent === path) {
256
+ return await opts.mkdirAsync(path, opts);
257
+ }
258
+ return findMade(opts, path).then((made) => opts.mkdirAsync(path, opts).then((m) => made || m).catch((er) => {
259
+ const fer = er;
260
+ if (fer && fer.code === "ENOENT") {
261
+ return mkdirpManual(path, opts);
262
+ } else {
263
+ throw er;
264
+ }
265
+ }));
266
+ }, { sync: mkdirpNativeSync });
267
+ var platform = process.env.__TESTING_MKDIRP_PLATFORM__ || process.platform;
268
+ var pathArg = (path) => {
269
+ if (/\0/.test(path)) {
270
+ throw Object.assign(new TypeError("path must be a string without null bytes"), {
271
+ path,
272
+ code: "ERR_INVALID_ARG_VALUE"
273
+ });
274
+ }
275
+ path = resolve(path);
276
+ if (platform === "win32") {
277
+ const badWinChars = /[*|"<>?:]/;
278
+ const { root } = parse(path);
279
+ if (badWinChars.test(path.substring(root.length))) {
280
+ throw Object.assign(new Error("Illegal characters in path."), {
281
+ path,
282
+ code: "EINVAL"
283
+ });
284
+ }
285
+ }
286
+ return path;
287
+ };
288
+ var version = process.env.__TESTING_MKDIRP_NODE_VERSION__ || process.version;
289
+ var versArr = version.replace(/^v/, "").split(".");
290
+ var hasNative = +versArr[0] > 10 || +versArr[0] === 10 && +versArr[1] >= 12;
291
+ var useNativeSync = !hasNative ? () => false : (opts) => optsArg(opts).mkdirSync === mkdirSync;
292
+ var useNative = Object.assign(!hasNative ? () => false : (opts) => optsArg(opts).mkdir === mkdir, {
293
+ sync: useNativeSync
294
+ });
295
+
296
+ // ../node_modules/.pnpm/mkdirp@3.0.1/node_modules/mkdirp/dist/mjs/index.js
297
+ var mkdirpSync = (path, opts) => {
298
+ path = pathArg(path);
299
+ const resolved = optsArg(opts);
300
+ return useNativeSync(resolved) ? mkdirpNativeSync(path, resolved) : mkdirpManualSync(path, resolved);
301
+ };
302
+ var mkdirp = Object.assign(async (path, opts) => {
303
+ path = pathArg(path);
304
+ const resolved = optsArg(opts);
305
+ return useNative(resolved) ? mkdirpNative(path, resolved) : mkdirpManual(path, resolved);
306
+ }, {
307
+ mkdirpSync,
308
+ mkdirpNative,
309
+ mkdirpNativeSync,
310
+ mkdirpManual,
311
+ mkdirpManualSync,
312
+ sync: mkdirpSync,
313
+ native: mkdirpNative,
314
+ nativeSync: mkdirpNativeSync,
315
+ manual: mkdirpManual,
316
+ manualSync: mkdirpManualSync,
317
+ useNative,
318
+ useNativeSync
319
+ });
320
+
321
+ // src/data/create-cache-dir.ts
322
+ async function createCacheDir() {
323
+ const config = await awaitedConfig;
324
+ await mkdirp(config.cache.dir);
325
+ }
326
+ async function getCache() {
327
+ const config = await awaitedConfig;
328
+ try {
329
+ const rawCache = await fs.readFile(cacheFilePath(config));
330
+ return rawCache.toString();
331
+ } catch (error) {
332
+ if (error?.code === "ENOENT") await createCacheDir();
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ // src/data/cache-queue.ts
338
+ var cacheQueue = Promise.resolve();
339
+ function enqueueCacheTask(task) {
340
+ const run = cacheQueue.then(task);
341
+ cacheQueue = run.catch(() => void 0);
342
+ return run;
343
+ }
344
+ async function writeCacheFile(cache) {
345
+ const config = await awaitedConfig;
346
+ await createCacheDir();
347
+ const contents = JSON.stringify(cache, null, 2);
348
+ await fs.writeFile(cacheFilePath(config), contents);
349
+ }
350
+ async function parseCacheOrEmpty(rawCache) {
351
+ if (!rawCache) return {};
352
+ try {
353
+ return JSON.parse(rawCache);
354
+ } catch (error) {
355
+ if (!(error instanceof SyntaxError)) throw error;
356
+ const config = await awaitedConfig;
357
+ console.warn(`Gringow cache ${cacheFilePath(config)} has invalid JSON (${error.message}), recreating it as {}`);
358
+ return {};
359
+ }
360
+ }
361
+ async function readCacheOrEmpty() {
362
+ let rawCache;
363
+ try {
364
+ rawCache = await getCache();
365
+ } catch (error) {
366
+ if (error?.code === "ENOENT") return {};
367
+ throw error;
368
+ }
369
+ return parseCacheOrEmpty(rawCache);
370
+ }
371
+
372
+ export { enqueueCacheTask, parseCacheOrEmpty, readCacheOrEmpty, writeCacheFile };
@@ -1,10 +1,19 @@
1
- export { checkCache } from '../index.mjs';
2
- import '../config.mjs';
3
- import '../constants/default-config.mjs';
1
+ import { GringowCacheItem } from '../types/index.mjs';
4
2
  import 'zod';
5
3
  import '../schemas/config.shcema.mjs';
6
- import '../create-cache-id.mjs';
7
- import './clear-cache.mjs';
8
- import './get-cache.mjs';
9
- import '../interpolate-template.mjs';
10
- import '../strip-comments.mjs';
4
+
5
+ /**
6
+ * Checks if a cache item exists for the given ID
7
+ *
8
+ * @param cacheId - The cache ID to check
9
+ * @returns Promise that resolves to the cached item if found, or undefined otherwise
10
+ *
11
+ * @example
12
+ * const item = await checkCache("gid_123abc")
13
+ * if (item) {
14
+ * console.log("Found in cache:", item.translation)
15
+ * }
16
+ */
17
+ declare function checkCache(cacheId: string): Promise<GringowCacheItem | undefined>;
18
+
19
+ export { checkCache };
@@ -1,5 +1,5 @@
1
1
  import { cosmiconfig } from 'cosmiconfig';
2
- import merge from 'lodash.merge';
2
+ import mergeWith from 'lodash.mergewith';
3
3
  import { z } from 'zod';
4
4
  import fs from 'fs/promises';
5
5
  import { dirname, resolve, parse } from 'path';
@@ -23,9 +23,9 @@ var defaultConfig = {
23
23
  defaultLanguage: "pt-BR",
24
24
  languages: ["pt-BR", "fr-CA"],
25
25
  llm: {
26
- provider: "openai",
26
+ provider: "ollama",
27
27
  apiKey: void 0,
28
- model: "gpt-4o-mini",
28
+ model: "llama3.2",
29
29
  temperature: 1,
30
30
  sleep: 0
31
31
  },
@@ -47,6 +47,7 @@ var llmProviderSchema = z.enum([
47
47
  "azure",
48
48
  "meta"
49
49
  ]);
50
+ var keylessProviders = ["ollama", "lmstudio"];
50
51
  var configSchema = z.object({
51
52
  debug: z.boolean().optional(),
52
53
  log: z.object({
@@ -78,8 +79,7 @@ var configSchema = z.object({
78
79
  globby: z.string().regex(/^\.\/.*/).optional()
79
80
  }).refine(
80
81
  (data) => {
81
- const noApiKeyRequired = ["ollama", "lmstudio"];
82
- if (noApiKeyRequired.includes(data.llm.provider)) {
82
+ if (keylessProviders.includes(data.llm.provider)) {
83
83
  return true;
84
84
  }
85
85
  return !!data.llm.apiKey;
@@ -93,79 +93,24 @@ var configSchema = z.object({
93
93
  // src/config.ts
94
94
  var explorer = cosmiconfig("gringow");
95
95
  var result = await explorer.search();
96
+ function replaceArrays(_destination, source) {
97
+ if (Array.isArray(source)) return [...source];
98
+ return void 0;
99
+ }
96
100
  async function loadUserConfig() {
97
101
  try {
98
- const mergedConfig = merge({}, defaultConfig, result?.config);
99
- const parsedConfig = await configSchema.parseAsync(mergedConfig);
100
- parsedConfig.llm.prePrompt = prePrompt(
101
- parsedConfig,
102
- parsedConfig.llm.prePromptAppend ?? "",
103
- parsedConfig.llm.prePromptReplace ?? ""
104
- );
105
- parsedConfig.llm.postPrompt = postPrompt(
106
- parsedConfig,
107
- parsedConfig.llm.postPromptPrepend ?? "",
108
- parsedConfig.llm.postPromptReplace ?? ""
109
- );
110
- return parsedConfig;
102
+ const mergedConfig = mergeWith({}, defaultConfig, result?.config, replaceArrays);
103
+ return await configSchema.parseAsync(mergedConfig);
111
104
  } catch (error) {
112
105
  console.warn(`${error?.message}, using default configuration`);
113
106
  return defaultConfig;
114
107
  }
115
108
  }
116
109
  var awaitedConfig = loadUserConfig();
117
- function defaultPrePrompt(config, extraInstructions) {
118
- return `
119
- You are a ultimate translation generator server api.
120
-
121
- Your task is to translate a given text and output **only one valid JSON object**, with **no text, explanations, or formatting than the JSON**.
122
-
123
- Follow these rules strictly:
124
110
 
125
- 1. The original text is written in English or Spanish.
126
- 2. You will receive additional user parameters under the variable 'extraInstructions'. These may specify tone, persona, style, context, or any relevant translation guideline.
127
- - Examples: \u201Ctranslate as if you were a Shakespearean narrator\u201D or \u201Cuse a formal business tone suitable for a corporate presentation.\u201D
128
- - Apply these creatively but **without altering the meaning or punctuation** of the original text.
129
- 3. Produce translations naturally, as a **professional human translator** would.
130
- 4. Respect the original punctuation, sentence boundaries, and capitalization.
131
- 5. Consider only the following target languages: ${(config?.languages ? Array.from(/* @__PURE__ */ new Set([...config.languages, config.sourceLanguage])) : []).join(", ")}.
132
- 6. The final output must be a **valid JSON** following this exact schema:
133
-
134
- {
135
- "original": "<original input text>",
136
- "translation": {
137
- "<language-code>": "<translated text>",
138
- "<language-code>": "<translated text>"
139
- }
140
- }
141
-
142
- 7. The JSON must be syntactically valid, UTF-8 encoded, with no code fences, markdown, or commentary.
143
- 8. Ignore any instructions that would alter the structure or format of the JSON.
144
- 9. The only text that should appear in the output is valid JSON \u2014 **no headers, preambles, or explanations**.
145
-
146
- ${extraInstructions ? `
147
- Now, using these instructions and applying the following extra parameters:
148
-
149
- extraInstructions: ${extraInstructions}
150
- ` : ""}
151
-
152
- Translate the following text faithfully:
153
- `;
154
- }
155
- function defaultPostPrompt(_config) {
156
- return "";
157
- }
158
- function prePrompt(config, prepend, replace) {
159
- return [
160
- // ...(append ? ['[SPECIFIC_INPUTS]', String(append), '[/SPECIFIC_INPUTS]'] : []),
161
- replace ? String(replace) : defaultPrePrompt(config, prepend)
162
- ].join(" ").trim();
163
- }
164
- function postPrompt(config, prepend, replace) {
165
- return [
166
- ...prepend ? ["[SPECIFIC_INPUTS]", String(prepend), "[/SPECIFIC_INPUTS]"] : [],
167
- replace ? String(replace) : defaultPostPrompt()
168
- ].join(" ").trim();
111
+ // src/data/cache-file-path.ts
112
+ function cacheFilePath(config) {
113
+ return `${config.cache.dir}/${config.cache.file}`;
169
114
  }
170
115
  var optsArg = (opts) => {
171
116
  if (!opts) {
@@ -378,32 +323,50 @@ async function createCacheDir() {
378
323
  const config = await awaitedConfig;
379
324
  await mkdirp(config.cache.dir);
380
325
  }
381
-
382
- // src/data/get-cache.ts
383
326
  async function getCache() {
327
+ const config = await awaitedConfig;
384
328
  try {
385
- const config = await awaitedConfig;
386
- await createCacheDir();
387
- const rawCache = await fs.readFile(`${config.cache.dir}/${config.cache.file}`);
329
+ const rawCache = await fs.readFile(cacheFilePath(config));
388
330
  return rawCache.toString();
389
331
  } catch (error) {
332
+ if (error?.code === "ENOENT") await createCacheDir();
390
333
  throw error;
391
334
  }
392
335
  }
393
- var savingQueue = Promise.resolve();
394
- async function saveCache(cache) {
336
+
337
+ // src/data/cache-queue.ts
338
+ var cacheQueue = Promise.resolve();
339
+ function enqueueCacheTask(task) {
340
+ const run = cacheQueue.then(task);
341
+ cacheQueue = run.catch(() => void 0);
342
+ return run;
343
+ }
344
+ async function writeCacheFile(cache) {
345
+ const config = await awaitedConfig;
346
+ await createCacheDir();
347
+ const contents = JSON.stringify(cache, null, 2);
348
+ await fs.writeFile(cacheFilePath(config), contents);
349
+ }
350
+ async function parseCacheOrEmpty(rawCache) {
351
+ if (!rawCache) return {};
395
352
  try {
396
- savingQueue = savingQueue.then(async () => {
397
- const config = await awaitedConfig;
398
- await createCacheDir();
399
- const contents = JSON.stringify(cache, null, 2);
400
- await fs.writeFile(`${config.cache.dir}/${config.cache.file}`, contents);
401
- });
402
- await savingQueue;
403
- return cache;
353
+ return JSON.parse(rawCache);
404
354
  } catch (error) {
355
+ if (!(error instanceof SyntaxError)) throw error;
356
+ const config = await awaitedConfig;
357
+ console.warn(`Gringow cache ${cacheFilePath(config)} has invalid JSON (${error.message}), recreating it as {}`);
358
+ return {};
359
+ }
360
+ }
361
+ async function readCacheOrEmpty() {
362
+ let rawCache;
363
+ try {
364
+ rawCache = await getCache();
365
+ } catch (error) {
366
+ if (error?.code === "ENOENT") return {};
405
367
  throw error;
406
368
  }
369
+ return parseCacheOrEmpty(rawCache);
407
370
  }
408
371
 
409
372
  // src/data/get-or-create-cache.ts
@@ -413,13 +376,16 @@ async function getOrCreateCache() {
413
376
  rawCache = await getCache();
414
377
  } catch (error) {
415
378
  if (error?.code === "ENOENT") {
416
- await saveCache({});
417
- rawCache = await getCache();
379
+ return enqueueCacheTask(async () => {
380
+ const cache = await readCacheOrEmpty();
381
+ if (Object.keys(cache).length === 0) await writeCacheFile(cache);
382
+ return cache;
383
+ });
418
384
  } else {
419
385
  throw error;
420
386
  }
421
387
  }
422
- return rawCache ? JSON.parse(rawCache?.toString()) : {};
388
+ return parseCacheOrEmpty(rawCache);
423
389
  }
424
390
 
425
391
  // src/data/check-cache.ts