rehype-og-card 1.1.1 → 2.0.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/README.md CHANGED
@@ -115,6 +115,14 @@ Path to save the build cache. For Astro projects, specify the `./node_modules/.a
115
115
  - Type: `string`
116
116
  - Default: `"./.cache/og-card"`
117
117
 
118
+ ### `buildCacheMaxAge`
119
+
120
+ Cache expiration time for build cache in milliseconds. Set to `false` to disable expiration.
121
+ Expiration is calculated using the cached timestamp stored in the cache itself.
122
+
123
+ - Type: `number | false`
124
+ - Default: `2592000000` (30 days)
125
+
118
126
  ### `crawlerUserAgent`
119
127
 
120
128
  User agent to use for fetching the OG metadata and images.
@@ -171,6 +179,14 @@ Path to save the server cache. Specify the directory whose contents will be copi
171
179
  - Type: `string`
172
180
  - Default: `"./public"`
173
181
 
182
+ ### `serverCacheMaxAge`
183
+
184
+ Cache expiration time for server cache in milliseconds. Set to `false` to disable expiration.
185
+ Expiration is calculated using the cached timestamp stored in the cache itself.
186
+
187
+ - Type: `number | false`
188
+ - Default: `2592000000` (30 days)
189
+
174
190
  ### `shortenURL`
175
191
 
176
192
  Whether to shorten the displayed URL.
package/dist/index.js CHANGED
@@ -5,8 +5,13 @@ import { restoreBuildCache, restoreOGDataBuildCache, saveBuildCacheFile, saveOGD
5
5
  import { isElement } from "hast-util-is-element";
6
6
  import path from "path";
7
7
  import { visitParents } from "unist-util-visit-parents";
8
+ // eslint-disable-next-line no-magic-numbers
9
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
10
+ // eslint-disable-next-line no-magic-numbers
11
+ const DEFAULT_CACHE_MAX_AGE_MS = 30 * ONE_DAY_MS;
8
12
  const DEFAULT_OPTIONS = {
9
13
  buildCache: false,
14
+ buildCacheMaxAge: DEFAULT_CACHE_MAX_AGE_MS,
10
15
  buildCachePath: "./node_modules/.cache",
11
16
  crawlerUserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
12
17
  decoding: "async",
@@ -15,6 +20,7 @@ const DEFAULT_OPTIONS = {
15
20
  loading: "lazy",
16
21
  openInNewTab: false,
17
22
  serverCache: true,
23
+ serverCacheMaxAge: DEFAULT_CACHE_MAX_AGE_MS,
18
24
  serverCachePath: "./public",
19
25
  shortenURL: true
20
26
  };
@@ -37,7 +43,7 @@ const rehypeOGCard = (options) => {
37
43
  const buildCacheExists = checkFileExistsSync(mergedOptions.buildCachePath);
38
44
  if (mergedOptions.buildCache) {
39
45
  if (buildCacheExists) {
40
- restoreBuildCache(mergedOptions.buildCachePath, mergedOptions.serverCachePath);
46
+ restoreBuildCache(mergedOptions.buildCachePath, mergedOptions.serverCachePath, mergedOptions.buildCacheMaxAge);
41
47
  }
42
48
  else {
43
49
  createDirectorySync(mergedOptions.buildCachePath);
@@ -91,7 +97,7 @@ const rehypeOGCard = (options) => {
91
97
  // eslint-disable-next-line jsdoc/require-jsdoc, max-statements, max-lines-per-function
92
98
  const linkCardPromise = async () => {
93
99
  let OGData = mergedOptions.buildCache
94
- ? await restoreOGDataBuildCache(anchorNode.properties.href, mergedOptions.buildCachePath)
100
+ ? await restoreOGDataBuildCache(anchorNode.properties.href, mergedOptions.buildCachePath, mergedOptions.buildCacheMaxAge)
95
101
  : null;
96
102
  if (!OGData) {
97
103
  OGData = await getOGData(anchorNode.properties.href, mergedOptions.crawlerUserAgent);
@@ -103,6 +109,7 @@ const rehypeOGCard = (options) => {
103
109
  }
104
110
  if (OGData.OGImageURL && mergedOptions.serverCache) {
105
111
  const filename = await downloadImage({
112
+ cacheMaxAge: mergedOptions.serverCacheMaxAge,
106
113
  directory: mergedOptions.serverCachePath,
107
114
  url: OGData.OGImageURL,
108
115
  userAgent: mergedOptions.crawlerUserAgent
@@ -118,6 +125,7 @@ const rehypeOGCard = (options) => {
118
125
  }
119
126
  if (OGData.faviconURL && mergedOptions.serverCache) {
120
127
  const filename = await downloadImage({
128
+ cacheMaxAge: mergedOptions.serverCacheMaxAge,
121
129
  directory: mergedOptions.serverCachePath,
122
130
  url: OGData.faviconURL,
123
131
  userAgent: mergedOptions.crawlerUserAgent
package/dist/types.d.ts CHANGED
@@ -41,6 +41,13 @@ interface RehypeOGCardOptions {
41
41
  * @default "./public"
42
42
  */
43
43
  serverCachePath?: string;
44
+ /**
45
+ * Cache expiration time for server cache in milliseconds.
46
+ * Set to `false` to disable expiration.
47
+ * Expiration is calculated from the cached timestamp stored in the cache itself.
48
+ * @default 2592000000
49
+ */
50
+ serverCacheMaxAge?: number | false;
44
51
  /**
45
52
  * Whether to cache the OG metadata and images for the next build.
46
53
  * `buildCache` option requires `serverCache` option to be enabled.
@@ -53,6 +60,13 @@ interface RehypeOGCardOptions {
53
60
  * @default "./node_modules/.cache"
54
61
  */
55
62
  buildCachePath?: string;
63
+ /**
64
+ * Cache expiration time for build cache in milliseconds.
65
+ * Set to `false` to disable expiration.
66
+ * Expiration is calculated from the cached timestamp stored in the cache itself.
67
+ * @default 2592000000
68
+ */
69
+ buildCacheMaxAge?: number | false;
56
70
  /**
57
71
  * User agent to use for fetching the OG metadata and images.
58
72
  * @default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
@@ -70,4 +84,10 @@ interface OGCardData {
70
84
  OGImageWidth?: number | undefined;
71
85
  OGImageHeight?: number | undefined;
72
86
  }
73
- export type { RehypeOGCardOptions, OGCardData };
87
+ declare const cacheIndexSchema: import("arktype/internal/variants/object.ts").ObjectType<{
88
+ [x: string]: {
89
+ createdAt: number;
90
+ };
91
+ }, {}>;
92
+ type CacheIndex = typeof cacheIndexSchema.infer;
93
+ export { cacheIndexSchema, type RehypeOGCardOptions, type OGCardData, type CacheIndex };
package/dist/types.js CHANGED
@@ -1 +1,7 @@
1
- export {};
1
+ import { type } from "arktype";
2
+ const cacheIndexSchema = type({
3
+ "[string]": {
4
+ createdAt: "number"
5
+ }
6
+ });
7
+ export { cacheIndexSchema };
@@ -1,4 +1,29 @@
1
- import type { OGCardData } from "../types.js";
1
+ import { type CacheIndex, type OGCardData } from "../types.js";
2
+ /**
3
+ * Check whether a cache entry is expired.
4
+ * @param cachedAtMs Cached timestamp in milliseconds.
5
+ * @param maxAgeMs Cache expiration time in milliseconds.
6
+ * @returns `true` if expired, otherwise `false`.
7
+ */
8
+ declare const isCacheExpired: (cachedAtMs: number, maxAgeMs: number | false) => boolean;
9
+ /**
10
+ * Get the cache index path for a directory.
11
+ * @param directory Cache directory path.
12
+ * @returns Cache index path.
13
+ */
14
+ declare const getCacheIndexPath: (directory: string) => string;
15
+ /**
16
+ * Read cache index.
17
+ * @param directory Cache directory path.
18
+ * @returns Cache index data.
19
+ */
20
+ declare const readCacheIndex: (directory: string) => Promise<CacheIndex>;
21
+ /**
22
+ * Write cache index.
23
+ * @param directory Cache directory path.
24
+ * @param index Cache index data.
25
+ */
26
+ declare const writeCacheIndex: (directory: string, index: CacheIndex) => Promise<void>;
2
27
  /**
3
28
  * Save build cache if server cache exists.
4
29
  * @param serverCachePath Server cache path.
@@ -9,8 +34,9 @@ declare const saveBuildCacheFile: (serverCachePath: string, buildCachePath: stri
9
34
  * Restore build cache if it exists.
10
35
  * @param buildCachePath Build cache path.
11
36
  * @param serverCachePath Server cache path.
37
+ * @param maxAgeMs Cache expiration time in milliseconds.
12
38
  */
13
- declare const restoreBuildCache: (buildCachePath: string, serverCachePath: string) => void;
39
+ declare const restoreBuildCache: (buildCachePath: string, serverCachePath: string, maxAgeMs: number | false) => void;
14
40
  /**
15
41
  * Save OG data to build cache.
16
42
  * @param url URL of the OG data.
@@ -22,7 +48,8 @@ declare const saveOGDataBuildCache: (url: string, OGData: OGCardData, buildCache
22
48
  * Restore OG data from build cache.
23
49
  * @param url URL of the OG data.
24
50
  * @param buildCachePath Build cache path.
51
+ * @param maxAgeMs Cache expiration time in milliseconds.
25
52
  * @returns Restored OG data. If not found, returns `null`.
26
53
  */
27
- declare const restoreOGDataBuildCache: (url: string, buildCachePath: string) => Promise<OGCardData | null>;
28
- export { saveBuildCacheFile, restoreBuildCache, saveOGDataBuildCache, restoreOGDataBuildCache };
54
+ declare const restoreOGDataBuildCache: (url: string, buildCachePath: string, maxAgeMs: number | false) => Promise<OGCardData | null>;
55
+ export { getCacheIndexPath, isCacheExpired, readCacheIndex, restoreBuildCache, restoreOGDataBuildCache, saveBuildCacheFile, saveOGDataBuildCache, writeCacheIndex };
@@ -1,25 +1,296 @@
1
- import { checkFileExists, copyDirectory, generateFilename } from "./file.js";
1
+ /* eslint-disable max-lines */
2
+ import { cacheIndexSchema } from "../types.js";
3
+ import { checkFileExists, generateFilename } from "./file.js";
2
4
  import fs from "fs/promises";
5
+ import fsSync from "fs";
3
6
  import path from "path";
7
+ import { setTimeout } from "timers/promises";
8
+ import { type } from "arktype";
9
+ /**
10
+ * Check whether a cache entry is expired.
11
+ * @param cachedAtMs Cached timestamp in milliseconds.
12
+ * @param maxAgeMs Cache expiration time in milliseconds.
13
+ * @returns `true` if expired, otherwise `false`.
14
+ */
15
+ const isCacheExpired = (cachedAtMs, maxAgeMs) => {
16
+ if (maxAgeMs === false)
17
+ return false;
18
+ return Date.now() - cachedAtMs > maxAgeMs;
19
+ };
20
+ const CACHE_INDEX_FILENAME = "cache.json";
21
+ const CACHE_INDEX_LOCK_FILENAME = "cache.json.lock";
22
+ /**
23
+ * Get the cache index path for a directory.
24
+ * @param directory Cache directory path.
25
+ * @returns Cache index path.
26
+ */
27
+ const getCacheIndexPath = (directory) => path.join(directory, CACHE_INDEX_FILENAME);
28
+ /**
29
+ * Get the cache index lock path for a directory.
30
+ * @param directory Cache directory path.
31
+ * @returns Cache index lock path.
32
+ */
33
+ const getCacheIndexLockPath = (directory) => path.join(directory, CACHE_INDEX_LOCK_FILENAME);
34
+ /**
35
+ * Sleep synchronously for a given duration.
36
+ * @param ms Duration in milliseconds.
37
+ */
38
+ const sleepSync = (ms) => {
39
+ // eslint-disable-next-line no-magic-numbers
40
+ const buffer = new SharedArrayBuffer(4);
41
+ const view = new Int32Array(buffer);
42
+ // eslint-disable-next-line no-magic-numbers
43
+ Atomics.wait(view, 0, 0, ms);
44
+ };
45
+ /**
46
+ * Acquire the cache index lock.
47
+ * @param directory Cache directory path.
48
+ * @returns Release function or `null` if locking failed.
49
+ */
50
+ const acquireCacheIndexLock = async (directory) => {
51
+ const lockPath = getCacheIndexLockPath(directory);
52
+ const maxRetries = 50;
53
+ const delayMs = 20;
54
+ const firstAttempt = 0;
55
+ const nextAttemptIncrement = 1;
56
+ /**
57
+ * Try acquiring the cache index lock.
58
+ * @param attempt Current attempt count.
59
+ * @returns Release function or `null` if locking failed.
60
+ */
61
+ const tryAcquire = async (attempt) => {
62
+ try {
63
+ const handle = await fs.open(lockPath, "wx");
64
+ await handle.close();
65
+ return async () => {
66
+ await fs.rm(lockPath, { force: true });
67
+ };
68
+ }
69
+ catch (error) {
70
+ const err = error;
71
+ if (err.code !== "EEXIST")
72
+ throw error;
73
+ if (attempt >= maxRetries)
74
+ return null;
75
+ await setTimeout(delayMs);
76
+ return tryAcquire(attempt + nextAttemptIncrement);
77
+ }
78
+ };
79
+ return tryAcquire(firstAttempt);
80
+ };
81
+ /**
82
+ * Acquire the cache index lock synchronously.
83
+ * @param directory Cache directory path.
84
+ * @returns Release function or `null` if locking failed.
85
+ */
86
+ const acquireCacheIndexLockSync = (directory) => {
87
+ const lockPath = getCacheIndexLockPath(directory);
88
+ const maxRetries = 50;
89
+ const delayMs = 20;
90
+ const firstAttempt = 0;
91
+ const nextAttemptIncrement = 1;
92
+ /**
93
+ * Try acquiring the cache index lock synchronously.
94
+ * @param attempt Current attempt count.
95
+ * @returns Release function or `null` if locking failed.
96
+ */
97
+ const tryAcquire = (attempt) => {
98
+ try {
99
+ const fd = fsSync.openSync(lockPath, "wx");
100
+ fsSync.closeSync(fd);
101
+ return () => {
102
+ fsSync.rmSync(lockPath, { force: true });
103
+ };
104
+ }
105
+ catch (error) {
106
+ const err = error;
107
+ if (err.code !== "EEXIST")
108
+ throw error;
109
+ if (attempt >= maxRetries)
110
+ return null;
111
+ sleepSync(delayMs);
112
+ return tryAcquire(attempt + nextAttemptIncrement);
113
+ }
114
+ };
115
+ return tryAcquire(firstAttempt);
116
+ };
117
+ /**
118
+ * Read cache index synchronously.
119
+ * @param directory Cache directory path.
120
+ * @returns Cache index data.
121
+ */
122
+ const readCacheIndexSync = (directory) => {
123
+ const indexPath = getCacheIndexPath(directory);
124
+ try {
125
+ const raw = fsSync.readFileSync(indexPath, "utf-8");
126
+ const parsed = JSON.parse(raw);
127
+ const validated = cacheIndexSchema(parsed);
128
+ if (validated instanceof type.errors)
129
+ return {};
130
+ return validated;
131
+ }
132
+ catch {
133
+ return {};
134
+ }
135
+ };
136
+ /**
137
+ * Write cache index synchronously.
138
+ * @param directory Cache directory path.
139
+ * @param index Cache index data.
140
+ */
141
+ const writeCacheIndexSync = (directory, index) => {
142
+ const indexPath = getCacheIndexPath(directory);
143
+ const tempPath = `${indexPath}.tmp`;
144
+ const releaseLock = acquireCacheIndexLockSync(directory);
145
+ try {
146
+ fsSync.writeFileSync(tempPath, JSON.stringify(index));
147
+ fsSync.renameSync(tempPath, indexPath);
148
+ }
149
+ finally {
150
+ releaseLock?.();
151
+ }
152
+ };
153
+ /**
154
+ * Read cache index.
155
+ * @param directory Cache directory path.
156
+ * @returns Cache index data.
157
+ */
158
+ const readCacheIndex = async (directory) => {
159
+ const indexPath = getCacheIndexPath(directory);
160
+ const exists = await checkFileExists(indexPath);
161
+ if (!exists)
162
+ return {};
163
+ try {
164
+ const raw = await fs.readFile(indexPath, "utf-8");
165
+ const parsed = JSON.parse(raw);
166
+ const validated = cacheIndexSchema(parsed);
167
+ if (validated instanceof type.errors)
168
+ return {};
169
+ return validated;
170
+ }
171
+ catch {
172
+ return {};
173
+ }
174
+ };
175
+ /**
176
+ * Write cache index.
177
+ * @param directory Cache directory path.
178
+ * @param index Cache index data.
179
+ */
180
+ const writeCacheIndex = async (directory, index) => {
181
+ const indexPath = getCacheIndexPath(directory);
182
+ const tempPath = `${indexPath}.tmp`;
183
+ const releaseLock = await acquireCacheIndexLock(directory);
184
+ try {
185
+ await fs.writeFile(tempPath, JSON.stringify(index));
186
+ await fs.rename(tempPath, indexPath);
187
+ }
188
+ finally {
189
+ await releaseLock?.();
190
+ }
191
+ };
192
+ /**
193
+ * Remove a cache file synchronously.
194
+ * @param directory Cache directory path.
195
+ * @param filename Cache filename.
196
+ */
197
+ const removeCacheEntrySync = (directory, filename) => {
198
+ const filePath = path.join(directory, filename);
199
+ fsSync.rmSync(filePath, { force: true });
200
+ };
201
+ /**
202
+ * Remove expired cache files synchronously.
203
+ * @param directory Cache directory path.
204
+ * @param maxAgeMs Cache expiration time in milliseconds.
205
+ */
206
+ // eslint-disable-next-line max-statements, max-lines-per-function
207
+ const pruneExpiredCacheFilesSync = (directory, maxAgeMs) => {
208
+ if (maxAgeMs === false)
209
+ return;
210
+ if (!fsSync.existsSync(directory))
211
+ return;
212
+ const index = readCacheIndexSync(directory);
213
+ let indexUpdated = false;
214
+ const entries = fsSync.readdirSync(directory, { withFileTypes: true });
215
+ for (const entry of entries) {
216
+ const entryPath = path.join(directory, entry.name);
217
+ if (entry.isDirectory()) {
218
+ pruneExpiredCacheFilesSync(entryPath, maxAgeMs);
219
+ // eslint-disable-next-line no-continue
220
+ continue;
221
+ }
222
+ // eslint-disable-next-line no-continue
223
+ if (entry.name === CACHE_INDEX_FILENAME || entry.name === CACHE_INDEX_LOCK_FILENAME)
224
+ continue;
225
+ if (entry.name.endsWith(".json")) {
226
+ try {
227
+ const raw = fsSync.readFileSync(entryPath, "utf-8");
228
+ const data = JSON.parse(raw);
229
+ if (typeof data.cachedAt !== "number") {
230
+ fsSync.rmSync(entryPath, { force: true });
231
+ // eslint-disable-next-line no-continue
232
+ continue;
233
+ }
234
+ if (isCacheExpired(data.cachedAt, maxAgeMs)) {
235
+ fsSync.rmSync(entryPath, { force: true });
236
+ }
237
+ }
238
+ catch {
239
+ fsSync.rmSync(entryPath, { force: true });
240
+ }
241
+ // eslint-disable-next-line no-continue
242
+ continue;
243
+ }
244
+ const cachedAtMs = index[entry.name]?.createdAt;
245
+ if (typeof cachedAtMs !== "number" || isCacheExpired(cachedAtMs, maxAgeMs)) {
246
+ removeCacheEntrySync(directory, entry.name);
247
+ indexUpdated = true;
248
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
249
+ delete index[entry.name];
250
+ }
251
+ }
252
+ if (indexUpdated) {
253
+ writeCacheIndexSync(directory, index);
254
+ }
255
+ };
4
256
  /**
5
257
  * Save build cache if server cache exists.
6
258
  * @param serverCachePath Server cache path.
7
259
  * @param buildCachePath Build cache path.
8
260
  */
261
+ // eslint-disable-next-line max-statements
9
262
  const saveBuildCacheFile = async (serverCachePath, buildCachePath) => {
10
263
  const serverCacheExists = await checkFileExists(serverCachePath);
11
264
  const buildCacheExists = await checkFileExists(buildCachePath);
12
265
  if (serverCacheExists && !buildCacheExists) {
13
266
  await fs.copyFile(serverCachePath, buildCachePath);
14
267
  }
268
+ const buildCacheExistsAfter = await checkFileExists(buildCachePath);
269
+ if (!buildCacheExistsAfter)
270
+ return;
271
+ const serverDirectory = path.dirname(serverCachePath);
272
+ const buildDirectory = path.dirname(buildCachePath);
273
+ const filename = path.basename(buildCachePath);
274
+ const serverIndex = readCacheIndexSync(serverDirectory);
275
+ const cachedAt = typeof serverIndex[filename]?.createdAt === "number" ? serverIndex[filename].createdAt : Date.now();
276
+ const buildIndex = readCacheIndexSync(buildDirectory);
277
+ buildIndex[filename] = { createdAt: cachedAt };
278
+ writeCacheIndexSync(buildDirectory, buildIndex);
15
279
  };
16
280
  /**
17
281
  * Restore build cache if it exists.
18
282
  * @param buildCachePath Build cache path.
19
283
  * @param serverCachePath Server cache path.
284
+ * @param maxAgeMs Cache expiration time in milliseconds.
20
285
  */
21
- const restoreBuildCache = (buildCachePath, serverCachePath) => {
22
- copyDirectory(buildCachePath, serverCachePath);
286
+ const restoreBuildCache = (buildCachePath, serverCachePath, maxAgeMs) => {
287
+ if (!fsSync.existsSync(buildCachePath))
288
+ return;
289
+ pruneExpiredCacheFilesSync(buildCachePath, maxAgeMs);
290
+ if (!fsSync.existsSync(serverCachePath)) {
291
+ fsSync.mkdirSync(serverCachePath, { recursive: true });
292
+ }
293
+ fsSync.cpSync(buildCachePath, serverCachePath, { recursive: true });
23
294
  };
24
295
  /**
25
296
  * Save OG data to build cache.
@@ -34,23 +305,42 @@ const saveOGDataBuildCache = async (url, OGData, buildCachePath) => {
34
305
  if (!cacheDirectoryExists) {
35
306
  await fs.mkdir(buildCachePath, { recursive: true });
36
307
  }
37
- await fs.writeFile(savePath, JSON.stringify(OGData));
308
+ const payload = { ...OGData, cachedAt: Date.now() };
309
+ await fs.writeFile(savePath, JSON.stringify(payload));
38
310
  };
39
311
  /**
40
312
  * Restore OG data from build cache.
41
313
  * @param url URL of the OG data.
42
314
  * @param buildCachePath Build cache path.
315
+ * @param maxAgeMs Cache expiration time in milliseconds.
43
316
  * @returns Restored OG data. If not found, returns `null`.
44
317
  */
45
- const restoreOGDataBuildCache = async (url, buildCachePath) => {
318
+ // eslint-disable-next-line max-statements
319
+ const restoreOGDataBuildCache = async (url, buildCachePath, maxAgeMs) => {
46
320
  const filename = generateFilename(url, false);
47
321
  const savePath = `${path.join(buildCachePath, filename)}.json`;
48
322
  try {
49
323
  const data = await fs.readFile(savePath, "utf-8");
50
- return JSON.parse(data);
324
+ const parsed = JSON.parse(data);
325
+ if (typeof parsed.cachedAt !== "number") {
326
+ if (maxAgeMs === false) {
327
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
328
+ const { cachedAt: __, ...rest } = parsed;
329
+ return rest;
330
+ }
331
+ await fs.rm(savePath, { force: true });
332
+ return null;
333
+ }
334
+ if (isCacheExpired(parsed.cachedAt, maxAgeMs)) {
335
+ await fs.rm(savePath, { force: true });
336
+ return null;
337
+ }
338
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
339
+ const { cachedAt: __, ...rest } = parsed;
340
+ return rest;
51
341
  }
52
342
  catch {
53
343
  return null;
54
344
  }
55
345
  };
56
- export { saveBuildCacheFile, restoreBuildCache, saveOGDataBuildCache, restoreOGDataBuildCache };
346
+ export { getCacheIndexPath, isCacheExpired, readCacheIndex, restoreBuildCache, restoreOGDataBuildCache, saveBuildCacheFile, saveOGDataBuildCache, writeCacheIndex };
@@ -25,6 +25,11 @@ interface DownloadImageOptions {
25
25
  * User agent to use for fetching the image.
26
26
  */
27
27
  userAgent: string;
28
+ /**
29
+ * Cache expiration time in milliseconds.
30
+ * Set to `false` to disable expiration.
31
+ */
32
+ cacheMaxAge: number | false;
28
33
  }
29
34
  /**
30
35
  * Download image from given URL. If the image already exists, return the filename.
@@ -1,4 +1,5 @@
1
1
  import { checkFileExists, generateFilename } from "./file.js";
2
+ import { isCacheExpired, readCacheIndex, writeCacheIndex } from "./cache.js";
2
3
  import fetch from "node-fetch";
3
4
  import fs from "fs/promises";
4
5
  import path from "path";
@@ -73,7 +74,7 @@ const getOGData = async (url, userAgent) => {
73
74
  * @param options Options to download image.
74
75
  * @returns Filename of the downloaded image.
75
76
  */
76
- // eslint-disable-next-line max-statements
77
+ // eslint-disable-next-line max-statements, max-lines-per-function
77
78
  const downloadImage = async (options) => {
78
79
  if (!isValidURL(options.url)) {
79
80
  // eslint-disable-next-line no-console
@@ -85,8 +86,22 @@ const downloadImage = async (options) => {
85
86
  const savePath = path.posix.join(options.directory, filename);
86
87
  // If the file already exists, return the filename.
87
88
  const fileExists = await checkFileExists(savePath);
88
- if (fileExists)
89
- return filename;
89
+ if (fileExists) {
90
+ if (options.cacheMaxAge === false)
91
+ return filename;
92
+ const index = await readCacheIndex(options.directory);
93
+ const entry = index[filename];
94
+ const cachedAt = entry?.createdAt;
95
+ const maxAge = options.cacheMaxAge;
96
+ if (typeof cachedAt === "number" && !isCacheExpired(cachedAt, maxAge))
97
+ return filename;
98
+ if (typeof entry !== "undefined") {
99
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
100
+ delete index[filename];
101
+ await writeCacheIndex(options.directory, index);
102
+ }
103
+ await fs.rm(savePath, { force: true });
104
+ }
90
105
  const response = await fetch(options.url, {
91
106
  headers: {
92
107
  "user-agent": options.userAgent
@@ -101,6 +116,10 @@ const downloadImage = async (options) => {
101
116
  await fs.mkdir(options.directory, { recursive: true });
102
117
  }
103
118
  await fs.writeFile(savePath, buffer);
119
+ const cachedAt = Date.now();
120
+ const index = await readCacheIndex(options.directory);
121
+ index[filename] = { createdAt: cachedAt };
122
+ await writeCacheIndex(options.directory, index);
104
123
  return filename;
105
124
  }
106
125
  catch (error) {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "rehype-og-card",
3
- "version": "1.1.1",
3
+ "version": "2.0.1",
4
4
  "description": "Rehype plugin to convert text links to link cards",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "files": [
8
- "./dist"
8
+ "dist"
9
9
  ],
10
10
  "type": "module",
11
11
  "publishConfig": {
@@ -43,8 +43,8 @@
43
43
  "@changesets/changelog-github": "^0.5.0",
44
44
  "@changesets/cli": "^2.27.7",
45
45
  "@robot-inventor/eslint-config": "^11.0.0",
46
- "@robot-inventor/tsconfig-base": "^6.0.0",
47
- "@types/node": "^24.2.1",
46
+ "@robot-inventor/tsconfig-base": "^7.0.0",
47
+ "@types/node": "^25.0.2",
48
48
  "eslint": "^9.7.0",
49
49
  "hast-util-to-html": "^9.0.1",
50
50
  "prettier": "^3.3.3",
@@ -57,6 +57,7 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@types/hast": "^3.0.4",
60
+ "arktype": "^2.1.29",
60
61
  "hast-util-is-element": "^3.0.0",
61
62
  "hastscript": "^9.0.0",
62
63
  "node-fetch": "^3.3.2",