remove patches integrated into indiekit-endpoint-activitypub fork
Deploy Indiekit Server / deploy (push) Successful in 1m33s

This commit is contained in:
Sven
2026-05-17 09:58:42 +02:00
parent 981e4833d6
commit 68962a3a07
9 changed files with 0 additions and 728 deletions
-53
View File
@@ -1,53 +0,0 @@
/**
* Patch @rmdes/indiekit-endpoint-activitypub federation-setup.js:
* - personOptions.alsoKnownAs → personOptions.aliases (Fedify uses 'aliases')
* - Add movedTo → personOptions.successor (Fedify uses 'successor')
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
];
const MARKER = "// patch-actor-aliases-successor: applied";
const OLD = ` if (profile.alsoKnownAs?.length > 0) {
personOptions.alsoKnownAs = profile.alsoKnownAs.map((u) => new URL(u));
}`;
const NEW = ` if (profile.alsoKnownAs?.length > 0) {
personOptions.aliases = profile.alsoKnownAs.map((u) => new URL(u));
}
if (profile.movedTo) {
personOptions.successor = new URL(profile.movedTo);
}
${MARKER}`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
const src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-actor-aliases-successor: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(OLD)) {
console.log(`[postinstall] patch-actor-aliases-successor: target not found in ${filePath}`);
continue;
}
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
console.log(`[postinstall] patch-actor-aliases-successor: applied to ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-actor-aliases-successor: no target file found");
}
@@ -1,77 +0,0 @@
/**
* Patch: replace countDocuments() with estimatedDocumentCount() in
* federation-setup.js followers/following collection dispatchers.
*
* All four calls are already wrapped in cachedQuery(), but on cache miss
* countDocuments() runs a full aggregate scan. estimatedDocumentCount()
* reads collection metadata — O(1).
*
* Covers: ap_followers (×2) and ap_following (×2) in collection pagination
* and counter dispatchers.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/federation-setup.js",
];
const MARKER = "// [patch] collection-count-estimate";
const replacements = [
{
old: ` const t = await collections.ap_followers.countDocuments();
return [d, t];`,
new: ` const t = await collections.ap_followers.estimatedDocumentCount(); ${MARKER}
return [d, t];`,
},
{
old: ` return await collections.ap_followers.countDocuments();`,
new: ` return await collections.ap_followers.estimatedDocumentCount(); ${MARKER}`,
},
{
old: ` const t = await collections.ap_following.countDocuments();
return [d, t];`,
new: ` const t = await collections.ap_following.estimatedDocumentCount(); ${MARKER}
return [d, t];`,
},
{
old: ` return await collections.ap_following.countDocuments();`,
new: ` return await collections.ap_following.estimatedDocumentCount(); ${MARKER}`,
},
];
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
let src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-collection-count-estimate: already applied in ${filePath}`);
patched = true;
break;
}
let changed = 0;
for (const { old, new: replacement } of replacements) {
if (src.includes(old)) {
src = src.replace(old, replacement);
changed++;
}
}
if (changed === 0) {
console.log(`[postinstall] patch-ap-collection-count-estimate: target snippets not found in ${filePath}`);
continue;
}
await writeFile(filePath, src, "utf8");
console.log(`[postinstall] patch-ap-collection-count-estimate: applied ${changed}/4 replacements in ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-collection-count-estimate: no target file found");
}
@@ -1,61 +0,0 @@
/**
* Patch: extract DM content from object.source.content when object.content is empty.
*
* Bluesky DMs bridged via bsky.brid.gy arrive as Create activities wrapping a
* ChatMessage object. Bridgy Fed sends the message text in `source.content`
* (plain text, mediaType: text/plain) without a `content` field (HTML).
*
* The DM handler only checked `object.content`, so all bsky.brid.gy DMs were
* stored with empty content, making them unviewable in Mona.
*
* Fix: after extracting `rawHtml` from `object.content`, also read
* `object.source?.content` as a plain-text fallback for `contentText` when
* `rawHtml` is empty.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-handlers.js",
];
const MARKER = "// [patch] bsky-source-content-fallback";
const OLD = ` const rawHtml = object.content?.toString() || "";
const contentHtml = sanitizeContent(rawHtml);
const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 500);`;
const NEW = ` const rawHtml = object.content?.toString() || "";
const sourceText = object.source?.content?.toString() || ""; // [patch] bsky-source-content-fallback
const contentHtml = sanitizeContent(rawHtml);
const contentText = rawHtml
? rawHtml.replace(/<[^>]*>/g, "").substring(0, 500)
: sourceText.substring(0, 500);`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
const src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-dm-bsky-source-content: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(OLD)) {
console.log(`[postinstall] patch-ap-dm-bsky-source-content: target snippet not found in ${filePath}`);
continue;
}
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
console.log(`[postinstall] patch-ap-dm-bsky-source-content: applied to ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-dm-bsky-source-content: no target file found");
}
-66
View File
@@ -1,66 +0,0 @@
/**
* Patch: capture addTimelineItem return value for DM serializeStatus.
*
* When sending an outbound DM via the Mastodon API, addTimelineItem returns
* the stored MongoDB document (which has _id). The return value was ignored,
* so the original timelineItem object had no _id. serializeStatus then crashed
* on item._id.toString() → "Cannot read properties of undefined (reading 'toString')".
*
* Fix: capture the addTimelineItem result and pass it to serializeStatus.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
];
const MARKER = "// [patch] dm-send-serialize-id";
const OLD = ` try {
await addTimelineItem(collections, timelineItem);
} catch (storeError) {
console.warn("[Mastodon API] Failed to store outbound DM in timeline:", storeError.message);
}
// Return a full serialized status so clients (Phanpy, Elk) can render it
const status = serializeStatus(timelineItem, {`;
const NEW = ` let storedDmItem = timelineItem; // [patch] dm-send-serialize-id
try {
const _dmResult = await addTimelineItem(collections, timelineItem);
if (_dmResult?._id) storedDmItem = _dmResult;
} catch (storeError) {
console.warn("[Mastodon API] Failed to store outbound DM in timeline:", storeError.message);
}
// Return a full serialized status so clients (Phanpy, Elk) can render it
const status = serializeStatus(storedDmItem, {`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
const src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-dm-send-serialize-id: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(OLD)) {
console.log(`[postinstall] patch-ap-dm-send-serialize-id: target snippet not found in ${filePath}`);
continue;
}
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
console.log(`[postinstall] patch-ap-dm-send-serialize-id: applied to ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-dm-send-serialize-id: no target file found");
}
@@ -1,52 +0,0 @@
/**
* Patch: replace ap_followers.countDocuments() with estimatedDocumentCount()
* in syndicator.js log statement.
*
* countDocuments() runs an aggregate({$match:{}, $group:{n:$sum:1}}) scan.
* For a logging call this is unnecessary — estimatedDocumentCount() reads
* collection metadata (O(1), no lock contention).
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
];
const MARKER = "// [patch] followers-count-estimate";
const OLD = ` // Count followers for logging
const followerCount =
await plugin._collections.ap_followers.countDocuments();`;
const NEW = ` // Count followers for logging
const followerCount =
await plugin._collections.ap_followers.estimatedDocumentCount(); ${MARKER}`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
const src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-followers-count-estimate: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(OLD)) {
console.log(`[postinstall] patch-ap-followers-count-estimate: target snippet not found in ${filePath}`);
continue;
}
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
console.log(`[postinstall] patch-ap-followers-count-estimate: applied to ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-followers-count-estimate: no target file found");
}
@@ -1,55 +0,0 @@
/**
* Patch: make touchKeyFreshness() fire-and-forget in inbox-listeners.js.
*
* touchKeyFreshness() is non-critical tracking (records lastSeenAt on
* ap_key_freshness). Under burst AP traffic, 11 parallel awaited upserts
* compete for MongoDB write locks — profiling shows 8-second delays on
* this single call while holding up inbox processing.
*
* Removing the await means inbox listeners don't block on the tracking
* write. The .catch(() => {}) mirrors the existing error handling inside
* the function itself.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-listeners.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/inbox-listeners.js",
];
const MARKER = "// [patch] key-freshness-nonblocking";
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
let src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-key-freshness-nonblocking: already applied in ${filePath}`);
patched = true;
break;
}
const before = src;
// Replace all: await touchKeyFreshness(...) → touchKeyFreshness(...).catch(() => {})
src = src.replace(
/await touchKeyFreshness\(([^)]+)\);/g,
`touchKeyFreshness($1).catch(() => {}); ${MARKER}`,
);
const count = (src.match(/key-freshness-nonblocking/g) || []).length;
if (count === 0) {
console.log(`[postinstall] patch-ap-key-freshness-nonblocking: target not found in ${filePath}`);
continue;
}
await writeFile(filePath, src, "utf8");
console.log(`[postinstall] patch-ap-key-freshness-nonblocking: applied to ${count} calls in ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-key-freshness-nonblocking: no target file found");
}
@@ -1,157 +0,0 @@
/**
* Patch: skip Micropub blog post creation for short standalone Mastodon posts.
*
* Very short status texts (≤ 280 chars, no media, no reply) are fediverse-only
* interactions — they federate via AP but must NOT create a permanent blog entry.
* Posts with in_reply_to, media, or text > 280 characters go through the full
* Micropub pipeline as before.
*
* Short posts get a canonical URL at /activitypub/objects/note/{uuid} (same as
* DMs), are stored in ap_timeline, and delivered to followers via Fedify's
* ctx.sendActivity({ "followers" }) — the same mechanism the AP syndicator uses.
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/routes/statuses.js",
];
const MARKER = "// [patch] short-post-bypass";
const OLD = ` // Resolve in_reply_to URL from status ID (cursor or ObjectId)
let inReplyTo = null;
if (inReplyToId) {`;
const NEW = ` // Short standalone posts (≤ 280 chars, no media, no reply) skip Micropub
// and go directly to AP — no blog entry is created. // [patch] short-post-bypass
const _shortPostLimit = 280;
const _isShortPost =
!inReplyToId &&
(!mediaIds || mediaIds.length === 0) &&
visibility !== "direct" &&
(statusText || "").trim().length > 0 &&
(statusText || "").trim().length <= _shortPostLimit;
if (_isShortPost) {
const _spPluginOptions = req.app.locals.mastodonPluginOptions || {};
const _spPublicationUrl = (_spPluginOptions.publicationUrl || baseUrl).replace(/\\/+$/, "");
const _spFederation = _spPluginOptions.federation;
const _spHandle = _spPluginOptions.handle || "user";
const _spHostname = (() => { try { return new URL(_spPublicationUrl).hostname; } catch { return ""; } })();
const _spProfile = await collections.ap_profile.findOne({});
const { randomUUID } = await import("node:crypto");
const _spUuid = randomUUID();
const _spNoteId = \`\${_spPublicationUrl}/activitypub/objects/note/\${_spUuid}\`;
const _spNow = new Date().toISOString();
const _spText = (statusText || "").trim();
const _spHtml = _spText
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/(https?:\\/\\/[^\\s<>&"')\\]]+)/g, '<a href="$1">$1</a>')
.replace(/\\n/g, "<br>");
const _spTlItem = await addTimelineItem(collections, {
uid: _spNoteId,
url: _spNoteId,
type: "note",
content: { text: _spText, html: \`<p>\${_spHtml}</p>\` },
author: {
name: _spProfile?.name || _spHandle,
url: _spProfile?.url || _spPublicationUrl,
photo: _spProfile?.icon || "",
handle: \`@\${_spHandle}@\${_spHostname}\`,
emojis: [],
bot: false,
},
published: _spNow,
createdAt: _spNow,
inReplyTo: null,
visibility: visibility || "public",
sensitive: sensitive === true || sensitive === "true",
category: [],
counts: { likes: 0, boosts: 0, replies: 0 },
});
if (_spFederation) {
try {
const { Note, Create } = await import("@fedify/fedify/vocab");
const _spCtx = _spFederation.createContext(
new URL(_spPublicationUrl + "/"),
{ handle: _spHandle, publicationUrl: _spPublicationUrl },
);
const _spActorUri = _spCtx.getActorUri(_spHandle);
const _spNote = new Note({
id: new URL(_spNoteId),
attributedTo: _spActorUri,
content: \`<p>\${_spHtml}</p>\`,
published: new Date(_spNow),
});
const _spCreate = new Create({
id: new URL(\`\${_spNoteId}#create\`),
actor: _spActorUri,
object: _spNote,
});
await _spCtx.sendActivity(
{ identifier: _spHandle },
"followers",
_spCreate,
{ preferSharedInbox: true, syncCollection: true, orderingKey: _spNoteId },
);
console.info(\`[Mastodon API] Short post federated to followers (no blog entry): \${_spNoteId}\`);
} catch (_spApErr) {
console.warn(\`[Mastodon API] Short post AP federation error: \${_spApErr.message}\`);
}
}
const _spStatus = serializeStatus(_spTlItem, {
baseUrl,
favouritedIds: new Set(),
rebloggedIds: new Set(),
bookmarkedIds: new Set(),
pinnedIds: new Set(),
});
if (idempotencyKey && collections.ap_idempotency) {
const { createHash } = await import("node:crypto");
const _spKey = createHash("sha256").update(\`\${baseUrl}:\${idempotencyKey}\`).digest("hex");
await collections.ap_idempotency.insertOne({ key: _spKey, response: _spStatus, createdAt: new Date() }).catch(() => {});
}
return res.json(_spStatus);
}
// Resolve in_reply_to URL from status ID (cursor or ObjectId)
let inReplyTo = null;
if (inReplyToId) {`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
const src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(OLD)) {
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: target snippet not found in ${filePath}`);
continue;
}
await writeFile(filePath, src.replace(OLD, NEW), "utf8");
console.log(`[postinstall] patch-ap-mastodon-short-post-bypass: applied to ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-mastodon-short-post-bypass: no target file found");
}
@@ -1,64 +0,0 @@
/**
* Patch: add a 2-minute deploy delay before ActivityPub syndication.
*
* When a post is created (via Micropub or Mastodon-API compose), the
* blog's deploy pipeline (Gitea → Eleventy → rsync) takes ~1-2 minutes.
* If ActivityPub fires immediately, followers receive a Create activity
* whose `url` still 404s — Mastodon clients show a broken link.
*
* This patch inserts a `setTimeout`-based delay at the top of the
* `syndicate()` function. The delay is configurable via the
* AP_SYNDICATE_DELAY_MS environment variable (default: 120000 = 2 min).
*/
import { access, readFile, writeFile } from "node:fs/promises";
const candidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/syndicator.js",
];
const MARKER = "// [patch] ap-syndicate-deploy-delay";
// The line right after the early-exit guards — insert before the
// try { const actorUrl = ... } block.
const INSERT_AFTER = ` try {
const actorUrl = plugin._getActorUrl();`;
const DELAY_BLOCK = ` // [patch] ap-syndicate-deploy-delay — wait for deploy pipeline before federating
const _apDelay = Number.parseInt(process.env.AP_SYNDICATE_DELAY_MS ?? "120000", 10);
if (_apDelay > 0) {
console.info(\`[ActivityPub] Delaying syndication by \${_apDelay}ms for \${properties.url}\`);
await new Promise((resolve) => setTimeout(resolve, _apDelay));
}
${MARKER}
try {
const actorUrl = plugin._getActorUrl();`;
async function exists(p) {
try { await access(p); return true; } catch { return false; }
}
let patched = false;
for (const filePath of candidates) {
if (!(await exists(filePath))) continue;
let src = await readFile(filePath, "utf8");
if (src.includes(MARKER)) {
console.log(`[postinstall] patch-ap-syndicate-deploy-delay: already applied in ${filePath}`);
patched = true;
break;
}
if (!src.includes(INSERT_AFTER)) {
console.log(`[postinstall] patch-ap-syndicate-deploy-delay: insertion point not found in ${filePath}`);
continue;
}
src = src.replace(INSERT_AFTER, DELAY_BLOCK);
await writeFile(filePath, src, "utf8");
console.log(`[postinstall] patch-ap-syndicate-deploy-delay: applied in ${filePath}`);
patched = true;
break;
}
if (!patched) {
console.log("[postinstall] patch-ap-syndicate-deploy-delay: no target file found");
}
@@ -1,143 +0,0 @@
import { access, readFile, writeFile } from "node:fs/promises";
// activitypub index.js and federation-setup.js unlisted guards are now
// built into the fork — only endpoint-syndicate (separate package) needs patching.
// backfill-timeline.js also needs patching — it runs on every startup and
// backfills ap_timeline from posts without filtering unlisted posts.
const endpointSyndicateCandidates = [
"node_modules/@indiekit/endpoint-syndicate/lib/utils.js",
"node_modules/@indiekit/indiekit/node_modules/@indiekit/endpoint-syndicate/lib/utils.js",
];
const backfillTimelineCandidates = [
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/mastodon/backfill-timeline.js",
];
const patchSpecs = [
{
name: "backfill-timeline-unlisted-guard",
candidates: backfillTimelineCandidates,
oldSnippet: ` const allPosts = await posts
.find({
"properties.post-status": { $ne: "draft" },
"properties.deleted": { $exists: false },
"properties.url": { $exists: true },
})
.toArray();`,
newSnippet: ` const allPosts = await posts
.find({
"properties.post-status": { $ne: "draft" },
"properties.deleted": { $exists: false },
"properties.url": { $exists: true },
// Exclude unlisted posts — they must not appear in ap_timeline (Mastodon API).
"properties.visibility": { $ne: "unlisted" },
})
.toArray();`,
},
{
name: "endpoint-syndicate-source-url-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` postData = await postsCollection.findOne({
"properties.url": url,
});`,
newSnippet: ` postData = await postsCollection.findOne({
"properties.url": url,
"properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
});`,
},
{
name: "endpoint-syndicate-get-post-data-pending-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` "properties.post-status": {
$ne: "draft",
},
})`,
newSnippet: ` "properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
})`,
},
{
name: "endpoint-syndicate-get-all-post-data-unlisted-guard",
candidates: endpointSyndicateCandidates,
oldSnippet: ` "properties.post-status": {
$ne: "draft",
},
})`,
newSnippet: ` "properties.post-status": {
$ne: "draft",
},
// Exclude unlisted posts from automatic syndication/federation.
"properties.visibility": {
$ne: "unlisted",
},
})`,
},
];
async function exists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
const checkedFiles = new Set();
const patchedFiles = new Set();
for (const spec of patchSpecs) {
let foundAnyTarget = false;
for (const filePath of spec.candidates) {
if (!(await exists(filePath))) {
continue;
}
foundAnyTarget = true;
checkedFiles.add(filePath);
const source = await readFile(filePath, "utf8");
let updated = source;
let replacements = 0;
if (source.includes(spec.oldSnippet)) {
updated = source.replace(spec.oldSnippet, spec.newSnippet);
replacements = 1;
}
if (replacements === 0 || updated === source) {
continue;
}
await writeFile(filePath, updated, "utf8");
patchedFiles.add(filePath);
}
if (!foundAnyTarget) {
console.log(`[postinstall] ${spec.name}: no target files found`);
}
}
if (checkedFiles.size === 0) {
console.log("[postinstall] No federation patch targets found");
} else if (patchedFiles.size === 0) {
console.log("[postinstall] federation unlisted guards already patched");
} else {
console.log(
`[postinstall] Patched federation unlisted guards in ${patchedFiles.size}/${checkedFiles.size} file(s)`,
);
}