Files
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

167 lines
6.1 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { ToolPackConfig } from "../src/config.js";
import { processWebSourcemaps } from "../src/web-sourcemaps.js";
/**
* These tests cover the parts of `processWebSourcemaps` that don't shell out
* to `@posthog/cli`:
*
* - missing chunks dir: returns silently
* - no .map files: returns silently
* - no credentials: ALWAYS strips .map files (the security guarantee)
*
* The upload-credentials-set path is not exercised here because it would have
* to either reach PostHog (network in unit tests is forbidden) or mock out
* `runPnpm`, which is intentionally an internal helper. The CLI happy-path
* runs in the release workflows themselves; this suite focuses on the
* strip-always invariant that must hold for both PR/fork builds and the rare
* case where the upload step fails inside the release.
*/
let tempRoot: string;
const SAVED_API_KEY = process.env.POSTHOG_CLI_API_KEY;
const SAVED_PROJECT_ID = process.env.POSTHOG_CLI_PROJECT_ID;
function restoreEnv(name: string, value: string | undefined): void {
if (value == null) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
beforeEach(async () => {
tempRoot = await mkdtemp(join(tmpdir(), "od-web-sourcemaps-"));
// Force the "no credentials" path so we test the strip-always invariant
// without needing to mock @posthog/cli or hit the network.
delete process.env.POSTHOG_CLI_API_KEY;
delete process.env.POSTHOG_CLI_PROJECT_ID;
delete process.env.POSTHOG_PERSONAL_API_KEY;
delete process.env.POSTHOG_PROJECT_ID;
});
afterEach(async () => {
if (tempRoot != null) {
await rm(tempRoot, { recursive: true, force: true });
}
restoreEnv("POSTHOG_CLI_API_KEY", SAVED_API_KEY);
restoreEnv("POSTHOG_CLI_PROJECT_ID", SAVED_PROJECT_ID);
});
function fakeConfig(workspaceRoot: string): ToolPackConfig {
return {
appVersion: "0.0.0-test",
containerized: false,
electronBuilderCliPath: "/dev/null",
electronDistPath: "/dev/null",
electronVersion: "0.0.0",
macCompression: "normal",
namespace: "test",
platform: "mac",
portable: false,
removeData: false,
removeLogs: false,
removeProductUserData: false,
removeSidecars: false,
requireVelaCli: false,
roots: {
output: {
appBuilderRoot: join(workspaceRoot, "out", "builder"),
namespaceRoot: join(workspaceRoot, "out", "ns"),
platformRoot: join(workspaceRoot, "out", "mac"),
root: join(workspaceRoot, "out"),
},
runtime: {
namespaceBaseRoot: join(workspaceRoot, "runtime"),
namespaceRoot: join(workspaceRoot, "runtime", "test"),
},
cacheRoot: join(workspaceRoot, "cache"),
toolPackRoot: join(workspaceRoot, "tools-pack"),
},
signed: false,
silent: true,
to: "all",
webOutputMode: "standalone",
workspaceRoot,
};
}
async function setupChunksDir(rootDir: string, mapNames: string[]): Promise<string> {
const chunksDir = join(rootDir, "apps", "web", ".next", "static");
await mkdir(join(chunksDir, "chunks"), { recursive: true });
// Always create a .js file paired with each .map so the layout matches what
// Next.js actually emits — otherwise a future helper change that filters by
// pairing would silently no-op the test.
for (const name of mapNames) {
const baseName = name.replace(/\.map$/, "");
await writeFile(join(chunksDir, "chunks", baseName), "/* fake bundle */\n", "utf8");
await writeFile(join(chunksDir, "chunks", name), '{"version":3,"sources":[]}\n', "utf8");
}
return chunksDir;
}
describe("processWebSourcemaps", () => {
it("returns silently when the browser chunks directory does not exist", async () => {
const config = fakeConfig(tempRoot);
await expect(processWebSourcemaps(config)).resolves.toBeUndefined();
});
it("returns silently when the chunks directory has no .map files", async () => {
const chunksDir = await setupChunksDir(tempRoot, []);
// Drop a non-map file so the dir is not empty but contains no sourcemaps.
await writeFile(join(chunksDir, "chunks", "main.js"), "/* */", "utf8");
const config = fakeConfig(tempRoot);
await expect(processWebSourcemaps(config)).resolves.toBeUndefined();
});
it("strips every .map file when credentials are missing (strip-only path)", async () => {
const chunksDir = await setupChunksDir(tempRoot, [
"framework-abc.js.map",
"main-app-def.js.map",
"polyfills-ghi.js.map",
]);
const config = fakeConfig(tempRoot);
await processWebSourcemaps(config);
// Every .map gone, every .js preserved.
await expect(
readFile(join(chunksDir, "chunks", "framework-abc.js.map"), "utf8"),
).rejects.toThrow();
await expect(
readFile(join(chunksDir, "chunks", "main-app-def.js.map"), "utf8"),
).rejects.toThrow();
await expect(
readFile(join(chunksDir, "chunks", "polyfills-ghi.js.map"), "utf8"),
).rejects.toThrow();
const preservedJs = await readFile(
join(chunksDir, "chunks", "framework-abc.js"),
"utf8",
);
expect(preservedJs).toContain("fake bundle");
});
it("strips .map files in nested subdirectories under .next/static", async () => {
const chunksDir = await setupChunksDir(tempRoot, []);
// Next.js puts some bundles under `.next/static/css` and `.next/static/media`
// even though the JS chunks live in `.next/static/chunks`. The strip walker
// must recurse — otherwise we'd leak CSS-source-style maps if Next ever
// emits them under those paths.
const nestedDir = join(chunksDir, "media");
await mkdir(nestedDir, { recursive: true });
await writeFile(join(nestedDir, "x.js"), "/* */", "utf8");
await writeFile(join(nestedDir, "x.js.map"), "{}", "utf8");
const config = fakeConfig(tempRoot);
await processWebSourcemaps(config);
await expect(readFile(join(nestedDir, "x.js.map"), "utf8")).rejects.toThrow();
await expect(readFile(join(nestedDir, "x.js"), "utf8")).resolves.toContain("/*");
});
});