perf(stickers): load the removal controls on demand, not with every sticker (#4164)

* perf(stickers): load the removal controls on demand, not with every sticker

StickerPlacementOverlay statically imported StickerPlacementHoverCard, which
value-imported placement.util for useForgetStickerPlacement, which pulled the
placement draft store and the free-offer helpers behind it. Every surface that
draws a sticker paid for that chain — the feed, post detail, image detail and
now article covers — including pages where no interactive sticker exists at all.

Measured over the value-import closure from StickerPlacementOverlay:
414,583 -> 357,306 bytes, 68 -> 64 modules. 57,277 bytes.

Split by consumer rather than by size. useForgetStickerPlacement has exactly two
callers, OwnerRemove and ModeratorRemove, both privileged and both inside the
dropdown — so they move to their own module and load on demand. `ssr: false` is
safe there because neither can render for a viewer the server has not authorised
anyway.

Deferring the hover card itself does not work, in either configuration. With ssr
on it prerenders, so the module is in the initial payload for hydration and
nothing is saved. With ssr off the client renders the loading placeholder first
— and the sticker artwork is the card's own `children`, so every sticker on the
page would blank until the chunk arrived.

Nothing else in the repo can report this regressing: restoring a static import
compiles, type checks, lints and renders identically, and the only symptom is
bytes. So a guard walks the real value-import closure rather than grepping for a
spelling of the import. It carries a positive control — a walker that resolved
nothing would report an empty closure and satisfy every negative case — and
asserts the controls are still reachable, since deleting them outright would
satisfy those cases too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(stickers): gate the moderator control at the call site, harden the guard

Review of the first commit found the deferral was doing less than it claimed and
the guard protecting it had three ways to be green and worthless.

ModeratorRemove rendered unconditionally, with the isModerator check inside the
component — so every viewer who opened a hover card fetched the chunk, not only
the privileged ones. The check is now at the call site as well, which is what
makes "almost nobody pays for this" true of the fetch rather than only of the
execution. The header comment claimed the latter and has been corrected.

The guard's walker matched `import` only, so `export { X } from` and `export *
from` — real bundled edges, one of which is already in this closure — brought
the whole chain back with every test still passing. It now follows re-exports,
and mutation-checking that edge turns eight cases red where it turned none.

Its negative cases were endsWith checks on filenames nothing asserted existed,
so renaming a cost module disarmed them permanently: a false green on a real
regression. Their existence is now asserted, and a rename fails loudly naming
the stale entry.

Its reachability case was satisfied by the dynamic() declarations alone, so
deleting the render block passed. It now derives the module and export names
from the import site, which survives a rename in either direction, and checks
the named exports exist — `.then(m => m.Missing)` renders nothing, silently,
with no type error.

It also anchors on the hover card as well as the overlay, since StickerHistory
Panel and StickerShopPanel already import the card directly and an overlay-only
anchor measures today's renderer rather than the property.

Comment stripping comes from test/strip-comments.ts rather than a third private
copy of that scanner. Reading import specifiers needs the strings kept, so this
adds a comments-only stripComments beside stripCommentsAndStrings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Justin Maier
2026-08-19 21:05:20 -06:00
committed by GitHub
parent a0f4a86fba
commit 3c92b9f28d
4 changed files with 483 additions and 188 deletions
@@ -1,32 +1,38 @@
import { Anchor, Badge, Group, HoverCard, Menu, Skeleton, Text, Tooltip } from '@mantine/core';
import { openConfirmModal } from '@mantine/modals';
import {
IconEye,
IconEyeOff,
IconFlag,
IconMessage,
IconShieldCancel,
IconSticker,
IconTrash,
} from '@tabler/icons-react';
import { IconEye, IconEyeOff, IconFlag, IconMessage, IconSticker } from '@tabler/icons-react';
import dynamic from 'next/dynamic';
import { LegacyActionIcon } from '~/components/LegacyActionIcon/LegacyActionIcon';
import { NextLink as Link } from '~/components/NextLink/NextLink';
import { openReportModal } from '~/components/Dialog/triggers/report';
import { useForgetStickerPlacement } from '~/components/Sticker/placement.util';
import {
moderatorTakedownConsequence,
removalConsequence,
removalLockReason,
} from '~/components/Sticker/payout-copy';
import { ReportEntity } from '~/shared/utils/report-helpers';
import type { ReactElement } from 'react';
import { useState } from 'react';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import { daysFromNow, formatDate } from '~/utils/date-helpers';
import { showErrorNotification, showSuccessNotification } from '~/utils/notifications';
import { showErrorNotification } from '~/utils/notifications';
import { trpc } from '~/utils/trpc';
// The only parts of this card that reach `placement.util`, and the placement
// draft store and free-offer helpers behind it. Deferring them takes that off
// the initial graph for every route that draws a sticker without rendering this
// card — the feed and article covers among them, though not the image detail
// page, which pulls the same chain through `ImageStickerOverlay` regardless.
//
// Both call sites gate on the viewer's role, which is what keeps the chunk
// unfetched rather than merely unrendered. `ssr: false` costs nothing here
// because neither can render for a viewer the server has not authorised, and
// neither is the card's `children` — deferring the card itself would blank the
// sticker artwork until the chunk arrived.
const OwnerRemove = dynamic(
() => import('~/components/Sticker/StickerPlacementRemoveActions').then((m) => m.OwnerRemove),
{ ssr: false }
);
const ModeratorRemove = dynamic(
() => import('~/components/Sticker/StickerPlacementRemoveActions').then((m) => m.ModeratorRemove),
{ ssr: false }
);
// Loaded with the hover, not with the page. The creator card drags in profile
// cosmetics, live metrics and edge media, and every image detail page renders
// this overlay whether or not anyone hovers a sticker.
@@ -82,6 +88,7 @@ export function StickerPlacementHoverCard({
pending?: boolean;
children: ReactElement;
}) {
const currentUser = useCurrentUser();
const [opened, setOpened] = useState(false);
const { data, isLoading, error } = trpc.placement.getStickerPlacementDetail.useQuery(
@@ -215,11 +222,16 @@ export function StickerPlacementHoverCard({
for as long as the page stays open — and that value would pick
the confirmation's sentence about the placer's money, right
before an irreversible click. */}
<ModeratorRemove
placementId={placementId}
pending={data.status === 'pending'}
free={data.free}
/>
{/* Gated here as well as inside the component: the guard within
decides whether it renders, but only this one decides whether
every hovering viewer downloads its chunk. */}
{currentUser?.isModerator && (
<ModeratorRemove
placementId={placementId}
pending={data.status === 'pending'}
free={data.free}
/>
)}
{data.viewerIsOwner && data.status === 'approved' && (
<OwnerRemove
placementId={placementId}
@@ -422,163 +434,3 @@ function HideNote({ placementId, commentHidden }: { placementId: number; comment
</Anchor>
);
}
/**
* The owner taking a sticker off their own image.
*
* Waits a week from approval, because approval already paid the owner and
* nothing is refunded: without the wait an owner could take the Buzz and wipe
* the sticker before anyone saw it. The refusal lives on the server — the
* disabled control and its date are what the card *says*, not what decides it.
*
* Shares the header slot with the moderator's remove rather than sitting in a
* footer of its own. A viewer has at most one of these powers, so the slot is
* never ambiguous, and two remove buttons in two corners read as one control
* duplicated.
*/
function OwnerRemove({
placementId,
removableAt,
free,
}: {
placementId: number;
removableAt: Date | string | null;
/**
* Whether this was placed against the creator's free capacity. Both sentences
* below are about money, and both are false when none moved.
*/
free: boolean;
}) {
const forget = useForgetStickerPlacement();
const remove = trpc.placement.actOnStickers.useMutation({
onSuccess: async () => {
showSuccessNotification({ message: 'Sticker removed.' });
await forget(placementId);
},
onError: (error) =>
showErrorNotification({ title: "Couldn't remove it", error: new Error(error.message) }),
});
const locked = !!removableAt;
return (
<Tooltip
withArrow
multiline
w={240}
label={
locked
? `${removalLockReason(free)} You can remove it from ${formatDate(removableAt as Date)}.`
: 'Takes the sticker off your image. No Buzz moves.'
}
>
{/* Wrapped, because a disabled control fires no pointer events and a
tooltip on it never opens — which would leave the date explaining the
button visible only to people who did not need it. */}
<span className="shrink-0">
<LegacyActionIcon
color="red"
variant="subtle"
size="sm"
aria-label="Remove this sticker from your image"
disabled={locked}
loading={remove.isPending}
onClick={() =>
openConfirmModal({
title: 'Remove this sticker',
children: <Text size="sm">{removalConsequence(free)}</Text>,
labels: { confirm: 'Remove', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => remove.mutate({ placementIds: [placementId], action: 'remove' }),
})
}
>
<IconTrash size={14} />
</LegacyActionIcon>
</span>
</Tooltip>
);
}
/**
* A moderator taking a sticker off the content it was placed on, from the
* sticker itself.
*
* The report queue is the route for something a user complained about; this is
* the route for a moderator who is looking at the image and can see the problem.
* Same mutation either way, so the two cannot drift into different rules about
* what removal means.
*
* On a live placement nothing else happens: no refund, and nobody is notified
* (Justin, 2026-08-08). The escrow was paid to a content owner who did not
* choose the sticker, and clawing it back would charge them for someone else's
* problem. **A pending PAID one is not that**: it settles as `removeByModerator`,
* whose payout is a forfeit of the whole escrow, fee and principal — so the
* confirmation has to say a different thing about the money.
*
* A free row has no escrow at all, in either state, so both of those sentences are
* false of one and the confirmation branches on `free` as well as on `pending`.
*
* Rendered for moderators only, which is convenience — `removePlacement` is a
* `moderatorProcedure`, so the refusal is on the mutation and stays there.
*/
function ModeratorRemove({
placementId,
pending = false,
free,
}: {
placementId: number;
pending?: boolean;
/** No escrow exists on a free row, so neither branch below is true of one. */
free: boolean;
}) {
const currentUser = useCurrentUser();
const forget = useForgetStickerPlacement();
const remove = trpc.placement.removePlacement.useMutation({
onSuccess: async (result) => {
// Reads the result rather than assuming it. The overlay can be drawing a
// placement someone else already settled, and reporting success on a
// takedown that removed nothing — beside a control that would have worked
// — is how a moderator concludes the sticker is handled.
showSuccessNotification({
message: result.removed
? 'Placement removed.'
: 'Nothing to remove — it had already been settled.',
});
await forget(placementId);
},
onError: (error) =>
showErrorNotification({ title: "Couldn't remove it", error: new Error(error.message) }),
});
if (!currentUser?.isModerator) return null;
return (
<Tooltip label="Take this sticker down as a moderator" withArrow>
<LegacyActionIcon
color="red"
variant="subtle"
size="sm"
className="shrink-0"
aria-label="Take down placement as moderator"
loading={remove.isPending}
onClick={() =>
openConfirmModal({
title: 'Take this placement down',
children: <Text size="sm">{moderatorTakedownConsequence({ pending, free })}</Text>,
labels: { confirm: 'Take down', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => remove.mutate({ placementId }),
})
}
>
{/* A shield, not a second bin. The owner's remove sits beside this one
for an account holding both powers, and two identical icons would
make the pair a coin toss over whose money moves. */}
<IconShieldCancel size={14} />
</LegacyActionIcon>
</Tooltip>
);
}
@@ -0,0 +1,189 @@
import { Text, Tooltip } from '@mantine/core';
import { openConfirmModal } from '@mantine/modals';
import { IconShieldCancel, IconTrash } from '@tabler/icons-react';
import { LegacyActionIcon } from '~/components/LegacyActionIcon/LegacyActionIcon';
import { useForgetStickerPlacement } from '~/components/Sticker/placement.util';
import {
moderatorTakedownConsequence,
removalConsequence,
removalLockReason,
} from '~/components/Sticker/payout-copy';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import { formatDate } from '~/utils/date-helpers';
import { showErrorNotification, showSuccessNotification } from '~/utils/notifications';
import { trpc } from '~/utils/trpc';
/**
* The two removal controls, in their own module so the hover card can load them
* on demand.
*
* They are the only things in the card that reach `placement.util`, which pulls
* the placement draft store and the free-offer helpers behind it — around 40 KB
* of source that every surface rendering a sticker paid for statically,
* including ones that never draw an interactive sticker at all. Both controls
* are privileged too: one needs the space's owner, the other a moderator, so for
* almost every viewer this code could never run.
*
* Split by consumer rather than by size. Deferring the whole card body would
* have deferred the sticker artwork with it, because the artwork is the card's
* own `children`.
*/
/**
* The owner taking a sticker off their own image.
*
* Waits a week from approval, because approval already paid the owner and
* nothing is refunded: without the wait an owner could take the Buzz and wipe
* the sticker before anyone saw it. The refusal lives on the server — the
* disabled control and its date are what the card *says*, not what decides it.
*
* Shares the header slot with the moderator's remove rather than sitting in a
* footer of its own. A viewer has at most one of these powers, so the slot is
* never ambiguous, and two remove buttons in two corners read as one control
* duplicated.
*/
export function OwnerRemove({
placementId,
removableAt,
free,
}: {
placementId: number;
removableAt: Date | string | null;
/**
* Whether this was placed against the creator's free capacity. Both sentences
* below are about money, and both are false when none moved.
*/
free: boolean;
}) {
const forget = useForgetStickerPlacement();
const remove = trpc.placement.actOnStickers.useMutation({
onSuccess: async () => {
showSuccessNotification({ message: 'Sticker removed.' });
await forget(placementId);
},
onError: (error) =>
showErrorNotification({ title: "Couldn't remove it", error: new Error(error.message) }),
});
const locked = !!removableAt;
return (
<Tooltip
withArrow
multiline
w={240}
label={
locked
? `${removalLockReason(free)} You can remove it from ${formatDate(removableAt as Date)}.`
: 'Takes the sticker off your image. No Buzz moves.'
}
>
{/* Wrapped, because a disabled control fires no pointer events and a
tooltip on it never opens — which would leave the date explaining the
button visible only to people who did not need it. */}
<span className="shrink-0">
<LegacyActionIcon
color="red"
variant="subtle"
size="sm"
aria-label="Remove this sticker from your image"
disabled={locked}
loading={remove.isPending}
onClick={() =>
openConfirmModal({
title: 'Remove this sticker',
children: <Text size="sm">{removalConsequence(free)}</Text>,
labels: { confirm: 'Remove', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => remove.mutate({ placementIds: [placementId], action: 'remove' }),
})
}
>
<IconTrash size={14} />
</LegacyActionIcon>
</span>
</Tooltip>
);
}
/**
* A moderator taking a sticker off the content it was placed on, from the
* sticker itself.
*
* The report queue is the route for something a user complained about; this is
* the route for a moderator who is looking at the image and can see the problem.
* Same mutation either way, so the two cannot drift into different rules about
* what removal means.
*
* On a live placement nothing else happens: no refund, and nobody is notified
* (Justin, 2026-08-08). The escrow was paid to a content owner who did not
* choose the sticker, and clawing it back would charge them for someone else's
* problem. **A pending PAID one is not that**: it settles as `removeByModerator`,
* whose payout is a forfeit of the whole escrow, fee and principal — so the
* confirmation has to say a different thing about the money.
*
* A free row has no escrow at all, in either state, so both of those sentences are
* false of one and the confirmation branches on `free` as well as on `pending`.
*
* Rendered for moderators only, which is convenience — `removePlacement` is a
* `moderatorProcedure`, so the refusal is on the mutation and stays there.
*/
export function ModeratorRemove({
placementId,
pending = false,
free,
}: {
placementId: number;
pending?: boolean;
/** No escrow exists on a free row, so neither branch below is true of one. */
free: boolean;
}) {
const currentUser = useCurrentUser();
const forget = useForgetStickerPlacement();
const remove = trpc.placement.removePlacement.useMutation({
onSuccess: async (result) => {
// Reads the result rather than assuming it. The overlay can be drawing a
// placement someone else already settled, and reporting success on a
// takedown that removed nothing — beside a control that would have worked
// — is how a moderator concludes the sticker is handled.
showSuccessNotification({
message: result.removed
? 'Placement removed.'
: 'Nothing to remove — it had already been settled.',
});
await forget(placementId);
},
onError: (error) =>
showErrorNotification({ title: "Couldn't remove it", error: new Error(error.message) }),
});
if (!currentUser?.isModerator) return null;
return (
<Tooltip label="Take this sticker down as a moderator" withArrow>
<LegacyActionIcon
color="red"
variant="subtle"
size="sm"
className="shrink-0"
aria-label="Take down placement as moderator"
loading={remove.isPending}
onClick={() =>
openConfirmModal({
title: 'Take this placement down',
children: <Text size="sm">{moderatorTakedownConsequence({ pending, free })}</Text>,
labels: { confirm: 'Take down', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () => remove.mutate({ placementId }),
})
}
>
{/* A shield, not a second bin. The owner's remove sits beside this one
for an account holding both powers, and two identical icons would
make the pair a coin toss over whose money moves. */}
<IconShieldCancel size={14} />
</LegacyActionIcon>
</Tooltip>
);
}
@@ -0,0 +1,247 @@
import { describe, expect, test } from 'vitest';
import { readFileSync, existsSync, statSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { stripComments, stripCommentsAndStrings } from '../../../../test/strip-comments';
/**
* 🔴 THE REMOVAL CONTROLS STAY OUT OF THE STATIC GRAPH.
*
* `StickerPlacementRemoveActions` is the only part of the hover card reaching
* `placement.util`, and behind it the placement draft store and the free-offer
* helpers. Deferring them takes four modules off the initial graph of every
* route that draws a sticker without rendering this card.
*
* Nothing else can report that regressing. Restoring a static import compiles,
* type checks, lints, renders identically and passes every other test in the
* repo; the only symptom is bytes.
*
* 🔴 WHAT THIS ASSERTS, AND WHAT IT DELIBERATELY DOES NOT. It asserts four named
* modules are absent from a **source** import graph. It is not a byte budget:
* source bytes are a ~5x overstatement of shipped bytes (~57 KB source is
* ~10.8 KB minified here), and the real ground truth for shipped weight is a
* production build, which no unit test can run. A budget written in the wrong
* unit would be worse than this, not better.
*
* 🔴 TWO ENTRY POINTS, NOT ONE. Anchoring only on the overlay would measure the
* surface that happens to render the card today. `StickerHistoryPanel` and
* `StickerShopPanel` already import the card directly, and a future consumer
* would bring the cost back on its own route while an overlay-anchored guard
* stayed green. The card's own closure is the entry-independent claim.
*
* 🔴 RE-EXPORTS ARE EDGES. `export { X } from '…'` and `export * from '…'` bundle
* exactly like an import, and this closure already contains one
* (`client-utils/cf-images-utils.ts`). Splitting a module and re-exporting from
* the old path is how this repo does splits — which is precisely what someone
* would do to `placement.util` next. A walker that only matched `import` would
* let all of it back with every test still green.
*/
const SRC = resolve(__dirname, '..', '..', '..');
const ENTRIES = {
overlay: join(SRC, 'components', 'Sticker', 'StickerPlacementOverlay.tsx'),
card: join(SRC, 'components', 'Sticker', 'StickerPlacementHoverCard.tsx'),
};
/**
* The modules the deferral exists to keep out, by path rather than by basename.
*
* Their existence is asserted in the positive control. A bare `endsWith` on a
* filename nothing checks would disarm itself permanently the day one of them is
* renamed — a false green on a real regression, which is the wrong direction to
* fail in.
*/
const COST_MODULES = [
join(SRC, 'components', 'Sticker', 'placement.util.ts'),
join(SRC, 'components', 'Sticker', 'free-offer.ts'),
join(SRC, 'store', 'sticker-placement-draft.store.ts'),
join(SRC, 'shared', 'utils', 'sticker-placement.ts'),
];
function resolveSpec(spec: string, fromFile: string): string | null {
let base: string;
if (spec.startsWith('~/')) base = join(SRC, spec.slice(2));
else if (spec.startsWith('.')) base = resolve(dirname(fromFile), spec);
else return null; // node_modules — not first-party source
for (const ext of ['.tsx', '.ts']) if (existsSync(base + ext)) return base + ext;
for (const ext of ['.tsx', '.ts']) {
const idx = join(base, `index${ext}`);
if (existsSync(idx)) return idx;
}
return existsSync(base) && statSync(base).isFile() ? base : null;
}
/**
* Static value edges: `import … from`, bare `import '…'`, and `export … from`.
*
* Comments are stripped first so a commented-out import is not an edge; strings
* are kept, because the specifier this is reading *is* a string. `import type`
* and `export type` are skipped — they erase, so a type-only edge costs nothing.
* `import(…)` is not matched, which is the mechanism under test.
*/
function staticEdges(source: string): string[] {
const code = stripComments(source);
const out: string[] = [];
const importFrom = /^\s*import\s+(?!type\s)[\s\S]*?from\s*['"]([^'"]+)['"]/gm;
const bareImport = /^\s*import\s*['"]([^'"]+)['"]/gm;
const exportFrom = /^\s*export\s+(?!type\s)[\s\S]*?from\s*['"]([^'"]+)['"]/gm;
const exportStar = /^\s*export\s*\*\s*(?:as\s+\w+\s*)?from\s*['"]([^'"]+)['"]/gm;
for (const re of [importFrom, bareImport, exportFrom, exportStar]) {
let match: RegExpExecArray | null;
re.lastIndex = 0;
while ((match = re.exec(code))) out.push(match[1]);
}
return out;
}
function closureFrom(entry: string): Set<string> {
const seen = new Set<string>();
const queue = [entry];
while (queue.length) {
const file = queue.pop() as string;
if (seen.has(file)) continue;
let source: string;
try {
source = readFileSync(file, 'utf8');
} catch {
continue;
}
seen.add(file);
for (const spec of staticEdges(source)) {
const next = resolveSpec(spec, file);
if (next && !seen.has(next)) queue.push(next);
}
}
return seen;
}
/** Dynamic specifiers, with the export names the call site pulls off them. */
function dynamicImports(source: string): Array<{ spec: string; exports: string[] }> {
const code = stripComments(source);
const found: Array<{ spec: string; exports: string[] }> = [];
const re = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
let match: RegExpExecArray | null;
while ((match = re.exec(code))) {
// The `.then(m => m.Name)` that usually follows, read as a window rather than
// a balanced match — the arrow's own parens defeat a naive `[^)]*`. Bounded
// at the statement end, or the window runs into the next dynamic import and
// attributes its exports to this module.
const after = code.slice(match.index + match[0].length);
const end = Math.min(
...[after.indexOf(';'), after.indexOf('import(')].filter((i) => i >= 0),
200
);
const exports = [...after.slice(0, end).matchAll(/\bm\.(\w+)/g)].map((m) => m[1]);
found.push({ spec: match[1], exports });
}
return found;
}
const closures = {
overlay: closureFrom(ENTRIES.overlay),
card: closureFrom(ENTRIES.card),
};
describe('the removal controls load on demand, not with every sticker', () => {
/**
* Without this the whole file is worthless twice over: a walker that resolved
* nothing reports an empty closure and satisfies every negative case, and a
* cost module that has been renamed is absent for the wrong reason.
*/
test('the walker resolved a real graph, and the cost modules still exist (positive control)', () => {
expect(closures.overlay.size, 'modules reached from StickerPlacementOverlay').toBeGreaterThan(
20
);
expect(closures.card.size, 'modules reached from StickerPlacementHoverCard').toBeGreaterThan(5);
// Depth 1 from the overlay, so this only proves the first hop resolved.
expect([...closures.overlay].some((f) => f.endsWith('StickerPlacementHoverCard.tsx'))).toBe(
true
);
// Reached only through the card, so a broken recursion is legible here
// rather than absorbed by the size threshold above.
expect(
[...closures.overlay].some((f) => f.endsWith(join('shared', 'utils', 'report-helpers.ts'))),
'a depth-2 module, reached only via the hover card'
).toBe(true);
for (const costModule of COST_MODULES) {
expect(
existsSync(costModule),
`${costModule} no longer exists — the negative cases below are asserting the absence of a file that cannot be present, and are silently vacuous. Update this list.`
).toBe(true);
}
});
for (const [name, entry] of Object.entries(ENTRIES)) {
for (const costModule of COST_MODULES) {
const label = costModule.slice(SRC.length + 1).replace(/\\/g, '/');
test(`🔴 ${label} is NOT in the static graph from the ${name}`, () => {
expect(
closures[name as keyof typeof closures].has(costModule),
`${label} is reached statically from ${entry.slice(
SRC.length + 1
)} again — the removal controls are back in the initial bundle`
).toBe(false);
});
}
}
/**
* The mirror of the negative claim: reachable dynamically, just not statically.
*
* Derived from the import sites rather than from a name written here, so it
* survives renaming the module or moving the controls elsewhere — and it does
* not pass on a reference that only exists in a comment or a string.
*/
test('the controls are still reachable, through a dynamic import', () => {
const card = readFileSync(ENTRIES.card, 'utf8');
const dynamics = dynamicImports(card);
expect(dynamics.length, 'dynamic imports in the hover card').toBeGreaterThan(0);
const reachesCost = dynamics.some(({ spec }) => {
const resolved = resolveSpec(spec, ENTRIES.card);
if (!resolved) return false;
const closure = closureFrom(resolved);
return COST_MODULES.some((costModule) => closure.has(costModule));
});
expect(
reachesCost,
'no dynamic import from the hover card reaches placement.util — the controls have been deleted rather than deferred, which would satisfy every negative case above'
).toBe(true);
});
test('every export the dynamic imports name actually exists', () => {
const card = readFileSync(ENTRIES.card, 'utf8');
let checked = 0;
for (const { spec, exports } of dynamicImports(card)) {
const resolved = resolveSpec(spec, ENTRIES.card);
if (!resolved) continue;
const target = stripCommentsAndStrings(readFileSync(resolved, 'utf8'));
for (const name of exports) {
checked++;
// `.then(m => m.Missing)` is `undefined` at runtime and renders nothing,
// silently — no type error, because the module is only known by its path.
expect(
new RegExp(`export\\s+(?:async\\s+)?(?:function|const|class)\\s+${name}\\b`).test(target),
`${spec} does not export ${name} — the dynamic import resolves to undefined and renders nothing`
).toBe(true);
}
}
expect(checked, 'named exports checked across the card dynamic imports').toBeGreaterThan(0);
});
test('the controls are rendered, not merely declared', () => {
// Stripped, because this repo has a recorded case of a token appearing in a
// JSDoc block counting as real wiring.
const card = stripCommentsAndStrings(readFileSync(ENTRIES.card, 'utf8'));
expect(card, 'the owner control is rendered').toMatch(/<OwnerRemove\b/);
expect(card, 'the moderator control is rendered').toMatch(/<ModeratorRemove\b/);
});
});
+13 -6
View File
@@ -26,14 +26,21 @@
* caller is required to carry. Under-stripping is the direction that produces a silent
* vacuous pass.
*/
/**
* Comments only, strings kept.
*
* For scans whose subject IS a string literal — an import specifier, a route
* path — where `stripCommentsAndStrings` would remove the very thing being
* looked for. Prose is still excluded, so a commented-out import does not count
* as an edge, which is the false-pass direction that matters for a graph walk.
*/
export function stripComments(source: string): string {
return source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/.*$/gm, '$1');
}
export function stripCommentsAndStrings(source: string): string {
return (
source
// Block comments (incl. JSDoc). Non-greedy so adjacent blocks stay separate.
.replace(/\/\*[\s\S]*?\*\//g, ' ')
// Line comments. The `[^:]` guard is the ledger's, and it is there so a `://` inside
// a URL is not treated as the start of a comment.
.replace(/(^|[^:])\/\/.*$/gm, '$1')
stripComments(source)
// Template literals (may span lines; an interpolation is stripped with them).
.replace(/`(?:[^`\\]|\\[\s\S])*`/g, ' ')
// Single- and double-quoted strings. `\n` is excluded so an unterminated quote