@fedify/backfill 2.3.0-dev.0
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/LICENSE +20 -0
- package/README.md +152 -0
- package/dist/backfill.test.cjs +1563 -0
- package/dist/backfill.test.d.cts +1 -0
- package/dist/backfill.test.d.ts +1 -0
- package/dist/backfill.test.js +1541 -0
- package/dist/mod.cjs +366 -0
- package/dist/mod.d.cts +157 -0
- package/dist/mod.d.ts +157 -0
- package/dist/mod.js +364 -0
- package/package.json +67 -0
package/dist/mod.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { Activity, Collection, CollectionPage, Create, Object as Object$1, OrderedCollection, OrderedCollectionPage } from "@fedify/vocab";
|
|
2
|
+
//#region src/backfill.ts
|
|
3
|
+
const defaultStrategies = ["context-auto"];
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 10;
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when backfill traversal exceeds the configured request budget.
|
|
7
|
+
*
|
|
8
|
+
* @since 2.3.0
|
|
9
|
+
*/
|
|
10
|
+
var MaxRequestsExceeded = class extends Error {};
|
|
11
|
+
/**
|
|
12
|
+
* Backfills post-like objects related to a seed object.
|
|
13
|
+
*
|
|
14
|
+
* The seed object is not yielded by default, but its ID is treated as already
|
|
15
|
+
* seen so it will not be yielded again if the collection contains it.
|
|
16
|
+
*
|
|
17
|
+
* @since 2.3.0
|
|
18
|
+
*/
|
|
19
|
+
async function* backfill(context, note, options = {}) {
|
|
20
|
+
if (options.maxItems != null && options.maxItems <= 0) return;
|
|
21
|
+
const strategies = normalizeStrategies(options.strategies);
|
|
22
|
+
if (strategies.length < 1) return;
|
|
23
|
+
const budget = {
|
|
24
|
+
signal: options.signal,
|
|
25
|
+
requestCount: 0,
|
|
26
|
+
documents: /* @__PURE__ */ new Map()
|
|
27
|
+
};
|
|
28
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
29
|
+
if (note.id != null) seenIds.add(note.id.href);
|
|
30
|
+
let yielded = 0;
|
|
31
|
+
try {
|
|
32
|
+
for (let i = 0; i < strategies.length; i++) {
|
|
33
|
+
const strategy = strategies[i];
|
|
34
|
+
let items;
|
|
35
|
+
if (isContextStrategy(strategy)) {
|
|
36
|
+
const contextStrategies = [strategy];
|
|
37
|
+
while (true) {
|
|
38
|
+
const nextStrategy = strategies[i + 1];
|
|
39
|
+
if (nextStrategy == null || !isContextStrategy(nextStrategy)) break;
|
|
40
|
+
contextStrategies.push(nextStrategy);
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
items = getContextStrategyItems(context, note, contextStrategies, options, budget, seenIds);
|
|
44
|
+
} else items = getStrategyItems(context, note, strategy, options, budget, seenIds);
|
|
45
|
+
for await (const item of items) {
|
|
46
|
+
const id = item.object.id ?? void 0;
|
|
47
|
+
if (id != null) {
|
|
48
|
+
if (seenIds.has(id.href)) continue;
|
|
49
|
+
seenIds.add(id.href);
|
|
50
|
+
}
|
|
51
|
+
options.signal?.throwIfAborted();
|
|
52
|
+
yield {
|
|
53
|
+
object: item.object,
|
|
54
|
+
id,
|
|
55
|
+
strategy: item.strategy,
|
|
56
|
+
origin: item.origin,
|
|
57
|
+
depth: item.depth
|
|
58
|
+
};
|
|
59
|
+
yielded++;
|
|
60
|
+
if (options.maxItems != null && yielded >= options.maxItems) return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error instanceof MaxRequestsExceeded) return;
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function normalizeStrategies(strategies = defaultStrategies) {
|
|
69
|
+
const normalized = [];
|
|
70
|
+
for (const strategy of strategies) if (strategy === "context-auto") {
|
|
71
|
+
for (let i = normalized.length - 1; i >= 0 && isContextStrategy(normalized[i]); i--) normalized.splice(i, 1);
|
|
72
|
+
if (!normalized.includes(strategy)) normalized.push(strategy);
|
|
73
|
+
} else if (isContextStrategy(strategy)) {
|
|
74
|
+
if (!currentContextGroupHasAuto(normalized) && !normalized.includes(strategy)) normalized.push(strategy);
|
|
75
|
+
} else if (!normalized.includes(strategy)) normalized.push(strategy);
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
function isContextStrategy(strategy) {
|
|
79
|
+
return strategy === "context-objects" || strategy === "context-activities" || strategy === "context-auto";
|
|
80
|
+
}
|
|
81
|
+
function currentContextGroupHasAuto(strategies) {
|
|
82
|
+
for (let i = strategies.length - 1; i >= 0; i--) {
|
|
83
|
+
const strategy = strategies[i];
|
|
84
|
+
if (!isContextStrategy(strategy)) return false;
|
|
85
|
+
if (strategy === "context-auto") return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
async function* getContextStrategyItems(context, note, strategies, options, budget, seenIds) {
|
|
90
|
+
const contextId = note.contextIds[0];
|
|
91
|
+
if (contextId == null) return;
|
|
92
|
+
const collection = await loadObject(context, contextId, options, budget);
|
|
93
|
+
if (!isCollection(collection)) return;
|
|
94
|
+
for await (const object of getCollectionItems(context, collection, options, budget, seenIds)) for (const strategy of strategies) for await (const item of getContextBackfillItems(context, object, strategy, options, budget)) yield {
|
|
95
|
+
object: item.object,
|
|
96
|
+
strategy: item.strategy,
|
|
97
|
+
origin: "collection",
|
|
98
|
+
depth: 0
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function* getStrategyItems(context, note, strategy, options, budget, seenIds) {
|
|
102
|
+
if (isContextStrategy(strategy)) yield* getContextStrategyItems(context, note, [strategy], options, budget, seenIds);
|
|
103
|
+
else if (strategy === "reply-tree") yield* getReplyTreeItems(context, note, options, budget);
|
|
104
|
+
}
|
|
105
|
+
async function* getReplyTreeItems(context, note, options, budget) {
|
|
106
|
+
const visitedObjectIds = /* @__PURE__ */ new Set();
|
|
107
|
+
const visitedObjects = /* @__PURE__ */ new WeakSet();
|
|
108
|
+
const visitedCollectionIds = /* @__PURE__ */ new Set();
|
|
109
|
+
const visitedCollections = /* @__PURE__ */ new WeakSet();
|
|
110
|
+
if (note.id != null) visitedObjectIds.add(note.id.href);
|
|
111
|
+
visitedObjects.add(note);
|
|
112
|
+
const ancestors = [];
|
|
113
|
+
for await (const item of getReplyAncestors(context, note, options, budget, {
|
|
114
|
+
depth: 1,
|
|
115
|
+
visitedObjectIds,
|
|
116
|
+
visitedObjects,
|
|
117
|
+
visitedCollectionIds,
|
|
118
|
+
visitedCollections
|
|
119
|
+
})) {
|
|
120
|
+
ancestors.push({
|
|
121
|
+
object: item.object,
|
|
122
|
+
depth: item.depth
|
|
123
|
+
});
|
|
124
|
+
yield item;
|
|
125
|
+
}
|
|
126
|
+
for (const ancestor of ancestors.toReversed()) yield* getReplyDescendants(context, ancestor.object, options, budget, {
|
|
127
|
+
depth: ancestor.depth + 1,
|
|
128
|
+
visitedObjectIds,
|
|
129
|
+
visitedObjects,
|
|
130
|
+
visitedCollectionIds,
|
|
131
|
+
visitedCollections
|
|
132
|
+
});
|
|
133
|
+
yield* getReplyDescendants(context, note, options, budget, {
|
|
134
|
+
depth: 1,
|
|
135
|
+
visitedObjectIds,
|
|
136
|
+
visitedObjects,
|
|
137
|
+
visitedCollectionIds,
|
|
138
|
+
visitedCollections
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function* getReplyAncestors(context, object, options, budget, traversal) {
|
|
142
|
+
if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
|
|
143
|
+
for await (const target of getReplyTargets(context, object, options, budget)) {
|
|
144
|
+
if (!isContextPostObject(target)) continue;
|
|
145
|
+
if (!visitReplyTreeObject(target, traversal)) continue;
|
|
146
|
+
yield {
|
|
147
|
+
object: target,
|
|
148
|
+
strategy: "reply-tree",
|
|
149
|
+
origin: "in-reply-to",
|
|
150
|
+
depth: traversal.depth
|
|
151
|
+
};
|
|
152
|
+
yield* getReplyAncestors(context, target, options, budget, {
|
|
153
|
+
depth: traversal.depth + 1,
|
|
154
|
+
visitedObjectIds: traversal.visitedObjectIds,
|
|
155
|
+
visitedObjects: traversal.visitedObjects,
|
|
156
|
+
visitedCollectionIds: traversal.visitedCollectionIds,
|
|
157
|
+
visitedCollections: traversal.visitedCollections
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function* getReplyDescendants(context, object, options, budget, traversal) {
|
|
162
|
+
if (traversal.depth > (options.maxDepth ?? DEFAULT_MAX_DEPTH)) return;
|
|
163
|
+
const repliesId = object.repliesId;
|
|
164
|
+
if (repliesId != null && traversal.visitedCollectionIds.has(repliesId.href)) return;
|
|
165
|
+
const replies = await getRepliesCollection(context, object, options, budget);
|
|
166
|
+
if (replies == null) return;
|
|
167
|
+
const unvisited = visitReplyTreeCollection(replies, traversal);
|
|
168
|
+
if (repliesId != null) traversal.visitedCollectionIds.add(repliesId.href);
|
|
169
|
+
if (!unvisited) return;
|
|
170
|
+
for await (const reply of getCollectionItems(context, replies, options, budget, traversal.visitedObjectIds)) {
|
|
171
|
+
if (!isContextPostObject(reply)) continue;
|
|
172
|
+
if (!visitReplyTreeObject(reply, traversal)) continue;
|
|
173
|
+
yield {
|
|
174
|
+
object: reply,
|
|
175
|
+
strategy: "reply-tree",
|
|
176
|
+
origin: "replies",
|
|
177
|
+
depth: traversal.depth
|
|
178
|
+
};
|
|
179
|
+
yield* getReplyDescendants(context, reply, options, budget, {
|
|
180
|
+
depth: traversal.depth + 1,
|
|
181
|
+
visitedObjectIds: traversal.visitedObjectIds,
|
|
182
|
+
visitedObjects: traversal.visitedObjects,
|
|
183
|
+
visitedCollectionIds: traversal.visitedCollectionIds,
|
|
184
|
+
visitedCollections: traversal.visitedCollections
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function* getReplyTargets(context, object, options, budget) {
|
|
189
|
+
try {
|
|
190
|
+
yield* object.getReplyTargets({
|
|
191
|
+
documentLoader: async (url) => {
|
|
192
|
+
return await loadCollectionItemDocument(context, url, options, budget);
|
|
193
|
+
},
|
|
194
|
+
crossOrigin: "trust"
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error instanceof MaxRequestsExceeded) throw error;
|
|
198
|
+
budget.signal?.throwIfAborted();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function getRepliesCollection(context, object, options, budget) {
|
|
202
|
+
try {
|
|
203
|
+
return await object.getReplies({
|
|
204
|
+
documentLoader: async (url) => {
|
|
205
|
+
return await loadCollectionItemDocument(context, url, options, budget);
|
|
206
|
+
},
|
|
207
|
+
crossOrigin: "trust"
|
|
208
|
+
});
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof MaxRequestsExceeded) throw error;
|
|
211
|
+
budget.signal?.throwIfAborted();
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function visitReplyTreeObject(object, traversal) {
|
|
216
|
+
if (object.id != null) {
|
|
217
|
+
if (traversal.visitedObjectIds.has(object.id.href)) return false;
|
|
218
|
+
traversal.visitedObjectIds.add(object.id.href);
|
|
219
|
+
} else if (traversal.visitedObjects.has(object)) return false;
|
|
220
|
+
traversal.visitedObjects.add(object);
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
function visitReplyTreeCollection(collection, traversal) {
|
|
224
|
+
if (collection.id != null) return visitReplyTreeCollectionId(collection.id, traversal);
|
|
225
|
+
else if (traversal.visitedCollections.has(collection)) return false;
|
|
226
|
+
traversal.visitedCollections.add(collection);
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
function visitReplyTreeCollectionId(id, traversal) {
|
|
230
|
+
if (traversal.visitedCollectionIds.has(id.href)) return false;
|
|
231
|
+
traversal.visitedCollectionIds.add(id.href);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
async function* getContextBackfillItems(context, object, strategy, options, budget) {
|
|
235
|
+
if (strategy === "context-objects" && isContextPostObject(object)) yield {
|
|
236
|
+
object,
|
|
237
|
+
strategy
|
|
238
|
+
};
|
|
239
|
+
else if (strategy === "context-activities") {
|
|
240
|
+
const activityObject = await getCreateActivityObject(context, object, options, budget);
|
|
241
|
+
if (activityObject != null && isContextPostObject(activityObject)) yield {
|
|
242
|
+
object: activityObject,
|
|
243
|
+
strategy
|
|
244
|
+
};
|
|
245
|
+
} else if (strategy === "context-auto") {
|
|
246
|
+
if (object instanceof Activity) {
|
|
247
|
+
const activityObject = await getCreateActivityObject(context, object, options, budget);
|
|
248
|
+
if (activityObject != null && isContextPostObject(activityObject)) yield {
|
|
249
|
+
object: activityObject,
|
|
250
|
+
strategy
|
|
251
|
+
};
|
|
252
|
+
} else if (isContextPostObject(object)) yield {
|
|
253
|
+
object,
|
|
254
|
+
strategy
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function* getCollectionItems(context, collection, options, budget, skipIds) {
|
|
259
|
+
yield* collection.getItems({
|
|
260
|
+
documentLoader: async (url) => {
|
|
261
|
+
return await loadCollectionItemDocument(context, url, options, budget, skipIds);
|
|
262
|
+
},
|
|
263
|
+
crossOrigin: "trust"
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
async function getCreateActivityObject(context, object, options, budget) {
|
|
267
|
+
if (!(object instanceof Create)) return null;
|
|
268
|
+
try {
|
|
269
|
+
return await object.getObject({
|
|
270
|
+
documentLoader: async (url) => {
|
|
271
|
+
return await loadCollectionItemDocument(context, url, options, budget);
|
|
272
|
+
},
|
|
273
|
+
crossOrigin: "trust"
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
if (error instanceof MaxRequestsExceeded) throw error;
|
|
277
|
+
budget.signal?.throwIfAborted();
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function loadCollectionItemDocument(context, url, options, budget, skipIds) {
|
|
282
|
+
let object;
|
|
283
|
+
try {
|
|
284
|
+
const iri = new URL(url);
|
|
285
|
+
if (skipIds?.has(iri.href)) return skippedCollectionItemDocument(url);
|
|
286
|
+
object = await loadObject(context, iri, options, budget, true);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (error instanceof MaxRequestsExceeded) throw error;
|
|
289
|
+
budget.signal?.throwIfAborted();
|
|
290
|
+
return skippedCollectionItemDocument(url);
|
|
291
|
+
}
|
|
292
|
+
if (object == null) return skippedCollectionItemDocument(url);
|
|
293
|
+
return {
|
|
294
|
+
contextUrl: null,
|
|
295
|
+
documentUrl: url,
|
|
296
|
+
document: await object.toJsonLd()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function skippedCollectionItemDocument(url) {
|
|
300
|
+
return {
|
|
301
|
+
contextUrl: null,
|
|
302
|
+
documentUrl: url,
|
|
303
|
+
document: {
|
|
304
|
+
"@context": "https://www.w3.org/ns/activitystreams",
|
|
305
|
+
type: "Activity"
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function loadObject(context, iri, options, budget, throwOnBudgetExceeded = false) {
|
|
310
|
+
budget.signal?.throwIfAborted();
|
|
311
|
+
const cacheKey = iri.href;
|
|
312
|
+
const cached = budget.documents.get(cacheKey);
|
|
313
|
+
if (cached != null) return await cached;
|
|
314
|
+
if (options.maxRequests != null && budget.requestCount >= options.maxRequests) {
|
|
315
|
+
if (throwOnBudgetExceeded) throw new MaxRequestsExceeded();
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
await waitForInterval(options, budget);
|
|
319
|
+
budget.signal?.throwIfAborted();
|
|
320
|
+
budget.requestCount++;
|
|
321
|
+
const document = context.documentLoader(iri, { signal: budget.signal });
|
|
322
|
+
budget.documents.set(cacheKey, document);
|
|
323
|
+
try {
|
|
324
|
+
return await document;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (budget.documents.get(cacheKey) === document) budget.documents.delete(cacheKey);
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function waitForInterval(options, budget) {
|
|
331
|
+
if (options.interval == null) return;
|
|
332
|
+
const milliseconds = durationToMilliseconds(typeof options.interval === "function" ? options.interval(budget.requestCount) : options.interval);
|
|
333
|
+
if (milliseconds <= 0) return;
|
|
334
|
+
await new Promise((resolve, reject) => {
|
|
335
|
+
if (budget.signal?.aborted) {
|
|
336
|
+
reject(budget.signal.reason);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const timeout = setTimeout(() => {
|
|
340
|
+
budget.signal?.removeEventListener("abort", onAbort);
|
|
341
|
+
resolve();
|
|
342
|
+
}, milliseconds);
|
|
343
|
+
const onAbort = () => {
|
|
344
|
+
clearTimeout(timeout);
|
|
345
|
+
reject(budget.signal?.reason);
|
|
346
|
+
};
|
|
347
|
+
budget.signal?.addEventListener("abort", onAbort, { once: true });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function durationToMilliseconds(duration) {
|
|
351
|
+
if (typeof duration === "string") {
|
|
352
|
+
if (typeof Temporal === "undefined") throw new TypeError("Temporal is not globally available; pass interval as a Temporal.DurationLike object instead of a string, or provide a Temporal polyfill.");
|
|
353
|
+
return Temporal.Duration.from(duration).total({ unit: "milliseconds" });
|
|
354
|
+
}
|
|
355
|
+
return (duration.milliseconds ?? 0) + (duration.seconds ?? 0) * 1e3 + (duration.minutes ?? 0) * 60 * 1e3 + (duration.hours ?? 0) * 60 * 60 * 1e3 + (duration.days ?? 0) * 24 * 60 * 60 * 1e3;
|
|
356
|
+
}
|
|
357
|
+
function isCollection(object) {
|
|
358
|
+
return object instanceof Collection || object instanceof OrderedCollection || object instanceof CollectionPage || object instanceof OrderedCollectionPage;
|
|
359
|
+
}
|
|
360
|
+
function isContextPostObject(object) {
|
|
361
|
+
return object instanceof Object$1 && !(object instanceof Activity) && !isCollection(object);
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
export { MaxRequestsExceeded, backfill };
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fedify/backfill",
|
|
3
|
+
"version": "2.3.0-dev.0",
|
|
4
|
+
"description": "ActivityPub backfill support for Fedify",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Fedify",
|
|
7
|
+
"ActivityPub",
|
|
8
|
+
"Fediverse",
|
|
9
|
+
"Backfill"
|
|
10
|
+
],
|
|
11
|
+
"author": {
|
|
12
|
+
"name": "Jiwon Kwon",
|
|
13
|
+
"email": "work@kwonjiwon.org"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://fedify.dev/",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/fedify-dev/fedify.git",
|
|
19
|
+
"directory": "packages/backfill"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/fedify-dev/fedify/issues"
|
|
24
|
+
},
|
|
25
|
+
"funding": [
|
|
26
|
+
"https://opencollective.com/fedify",
|
|
27
|
+
"https://github.com/sponsors/dahlia"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/mod.cjs",
|
|
31
|
+
"module": "./dist/mod.js",
|
|
32
|
+
"types": "./dist/mod.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": {
|
|
36
|
+
"import": "./dist/mod.d.ts",
|
|
37
|
+
"require": "./dist/mod.d.cts",
|
|
38
|
+
"default": "./dist/mod.d.ts"
|
|
39
|
+
},
|
|
40
|
+
"import": "./dist/mod.js",
|
|
41
|
+
"require": "./dist/mod.cjs",
|
|
42
|
+
"default": "./dist/mod.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"dist/",
|
|
48
|
+
"package.json",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@fedify/vocab": "2.3.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"tsdown": "^0.22.0",
|
|
56
|
+
"typescript": "^6.0.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build:self": "tsdown",
|
|
60
|
+
"build": "pnpm --filter @fedify/backfill... run build:self",
|
|
61
|
+
"prepublish": "pnpm build",
|
|
62
|
+
"pretest": "pnpm build",
|
|
63
|
+
"test": "cd dist/ && node --test",
|
|
64
|
+
"pretest:bun": "pnpm build",
|
|
65
|
+
"test:bun": "cd dist/ && bun test --timeout 60000"
|
|
66
|
+
}
|
|
67
|
+
}
|