Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/app/[...parts]/_page/NpmDiff/DiffTooLarge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type ComponentProps, forwardRef } from "react";
import Code from "^/components/ui/Code";
import Heading from "^/components/ui/Heading";
import Pkg from "^/components/ui/Pkg";
import { cx } from "^/lib/cva";
import type SimplePackageSpec from "^/lib/SimplePackageSpec";

export interface DiffTooLargeProps extends ComponentProps<"section"> {
a: SimplePackageSpec;
b: SimplePackageSpec;
specs: [string, string];
}

const DiffTooLarge = forwardRef<HTMLElement, DiffTooLargeProps>(
({ a, b, specs, className, ...props }, ref) => (
<section
className={cx("p-12 text-center", className!)}
ref={ref}
{...props}
>
<p className="text-4xl">📦🐘</p>
<Heading h={3} className="mb-4">
This diff is too large!
</Heading>
<p className="mb-4">
Comparing <Pkg pkg={a} /> and <Pkg pkg={b} /> took too long. You
can run it locally instead:
</p>
<Code>
npm diff --diff={specs[0]} --diff={specs[1]}
</Code>
</section>
),
);
DiffTooLarge.displayName = "DiffTooLarge";

export default DiffTooLarge;
4 changes: 4 additions & 0 deletions src/app/[...parts]/_page/NpmDiff/NpmDiff.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import npmDiff from "^/lib/npmDiff";
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
import countChanges from "^/lib/utils/countChanges";
import DiffFiles from "./DiffFiles";
import DiffTooLarge from "./DiffTooLarge";
import NoDiff from "./NoDiff";
import ViewTypeSwitch from "./ViewTypeSwitch";

Expand All @@ -26,6 +27,9 @@ const NpmDiff = async ({ a, b, specs, options }: NpmDiffProps) => {
const result = await npmDiff(specs, options);

if (!result.ok) {
if (result.kind === "timeout") {
return <DiffTooLarge a={a} b={b} specs={specs} />;
}
throw new Error(result.message);
}

Expand Down
7 changes: 6 additions & 1 deletion src/app/api/[...parts]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ export async function GET(req: NextRequest, { params }: DiffApiContext) {
headers: defaultPageCachingHeaders,
});
} else {
const status = result.kind === "not-found" ? 404 : 500;
const status =
result.kind === "not-found"
? 404
: result.kind === "timeout"
? 504
: 500;
return NextResponse.json(result.message, { status });
}
} else {
Expand Down
1 change: 1 addition & 0 deletions src/lib/npmDiff/NpmDiffError.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type NpmDiffError =
| { kind: "not-found"; message: string }
| { kind: "timeout"; message: string }
| { kind: "unknown"; message: string };
41 changes: 41 additions & 0 deletions src/lib/npmDiff/libnpmdiff.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import libnpmdiff, { type Options } from "libnpmdiff";
import { parentPort, workerData } from "node:worker_threads";

export interface LibnpmdiffWorkerData {
specs: [string, string];
options: Options;
}

export type LibnpmdiffWorkerMessage =
| { ok: true; diff: string }
| {
ok: false;
error: {
code?: unknown;
type?: unknown;
wanted?: unknown;
pkgid?: unknown;
message?: unknown;
};
};

const { specs, options } = workerData as LibnpmdiffWorkerData;

libnpmdiff(specs, options).then(
(diff) => {
parentPort?.postMessage({
ok: true,
diff,
} satisfies LibnpmdiffWorkerMessage);
},
(e: unknown) => {
const { code, type, wanted, pkgid, message } = (e ?? {}) as Record<
string,
unknown
>;
parentPort?.postMessage({
ok: false,
error: { code, type, wanted, pkgid, message },
} satisfies LibnpmdiffWorkerMessage);
},
);
17 changes: 15 additions & 2 deletions src/lib/npmDiff/npmDiff.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import libnpmdiff, { type Options } from "libnpmdiff";
import type { Options } from "libnpmdiff";
import { cacheLife } from "next/cache";
import { hasErrorCode } from "^/lib/utils/hasErrorCode";
import type { NpmDiffError } from "./NpmDiffError";
import { LibnpmdiffTimeoutError, runLibnpmdiff } from "./runLibnpmdiff";

export const NPM_DIFF_TIMEOUT_MS = 40_000;

export type NpmDiffResult =
{ ok: true; diff: string } | ({ ok: false } & NpmDiffError);
Expand Down Expand Up @@ -29,7 +32,7 @@ async function npmDiff(
let startTime = 0;
try {
startTime = Date.now();
const result = await libnpmdiff(specs, options);
const result = await runLibnpmdiff(specs, options, NPM_DIFF_TIMEOUT_MS);

cacheLife("max");

Expand All @@ -48,6 +51,16 @@ async function npmDiff(
duration: Date.now() - startTime,
});

if (e instanceof LibnpmdiffTimeoutError) {
cacheLife("days");

return {
ok: false,
kind: "timeout",
message: `The diff took too long to compute. Run "npm diff --diff=${specs[0]} --diff=${specs[1]}" locally instead.`,
};
}

const isEtarget = (e: unknown): e is ErrorETARGET =>
hasErrorCode(e, "ETARGET");
const isE404 = (e: unknown): e is Error404 => hasErrorCode(e, "E404");
Expand Down
53 changes: 53 additions & 0 deletions src/lib/npmDiff/runLibnpmdiff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Options } from "libnpmdiff";
import { Worker } from "node:worker_threads";
import type {
LibnpmdiffWorkerData,
LibnpmdiffWorkerMessage,
} from "./libnpmdiff.worker";

export class LibnpmdiffTimeoutError extends Error {
constructor(timeoutMs: number) {
super(`libnpmdiff did not finish within ${timeoutMs}ms`);
this.name = "LibnpmdiffTimeoutError";
}
}

export function runLibnpmdiff(
specs: [string, string],
options: Options,
timeoutMs: number,
): Promise<string> {
return new Promise((resolve, reject) => {
const worker = new Worker(
new URL("./libnpmdiff.worker.ts", import.meta.url),
{
workerData: { specs, options } satisfies LibnpmdiffWorkerData,
},
);

const timer = setTimeout(() => {
reject(new LibnpmdiffTimeoutError(timeoutMs));
void worker.terminate();
}, timeoutMs);

worker.once("message", (message: LibnpmdiffWorkerMessage) => {
clearTimeout(timer);
if (message.ok) {
resolve(message.diff);
} else {
reject(Object.assign(new Error(), message.error));
}
void worker.terminate();
});

worker.once("error", (e) => {
clearTimeout(timer);
reject(e);
});

worker.once("exit", (code) => {
clearTimeout(timer);
reject(new Error(`libnpmdiff worker exited with code ${code}`));
});
});
}
Loading