Merge branch 'release/v0.8.0' of github.com:nexu-io/open-design into release/v0.8.0

This commit is contained in:
lefarcen 2026-05-21 11:59:46 +08:00
commit 4f70893b18
10 changed files with 508 additions and 0 deletions

View file

@ -41,6 +41,13 @@ env:
# no events leave the user's machine.
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
# PostHog Error tracking sourcemap upload. Personal API key (phx_...) and
# project ID let tools-pack's web-sourcemaps step ship browser sourcemaps
# to PostHog after `next build` and before the .map files are stripped
# from the packaged bundle. Missing in PR/fork builds → upload is skipped
# and the helper still strips .map to keep source out of the installer.
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
jobs:
metadata:

View file

@ -19,6 +19,13 @@ env:
# leave the user's machine, /api/analytics/config returns enabled=false).
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
# PostHog Error tracking sourcemap upload. Personal API key (phx_...) and
# project ID let tools-pack's web-sourcemaps step ship browser sourcemaps
# to PostHog after `next build` and before the .map files are stripped
# from the packaged bundle. Missing in PR/fork builds → upload is skipped
# and the helper still strips .map to keep source out of the installer.
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
jobs:
metadata:

View file

@ -32,6 +32,13 @@ env:
# leave the user's machine, /api/analytics/config returns enabled=false).
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
POSTHOG_HOST: ${{ vars.POSTHOG_HOST }}
# PostHog Error tracking sourcemap upload. Personal API key (phx_...) and
# project ID let tools-pack's web-sourcemaps step ship browser sourcemaps
# to PostHog after `next build` and before the .map files are stripped
# from the packaged bundle. Missing in PR/fork builds → upload is skipped
# and the helper still strips .map to keep source out of the installer.
POSTHOG_CLI_API_KEY: ${{ secrets.POSTHOG_CLI_API_KEY }}
POSTHOG_CLI_PROJECT_ID: ${{ vars.POSTHOG_CLI_PROJECT_ID }}
jobs:
metadata:

View file

@ -105,6 +105,11 @@ const nextConfig: NextConfig = {
allowedDevOrigins: configuredAllowedDevHosts(),
outputFileTracingRoot: WORKSPACE_ROOT,
reactStrictMode: true,
// Emit browser sourcemaps so packaged-runtime exceptions can be symbolicated
// by PostHog. `tools/pack/src/web-sourcemaps.ts` runs after `next build`
// to inject chunk IDs, upload to PostHog, and ALWAYS delete the .map files
// before packaging so source never ships inside an installer.
productionBrowserSourceMaps: true,
turbopack: {
root: WORKSPACE_ROOT,
},

View file

@ -85,6 +85,32 @@ export type ToolPackConfig = {
*/
posthogKey?: string;
posthogHost?: string;
/**
* Personal API key (`phx_...`) used by the @posthog/cli sourcemap helper to
* upload browser sourcemaps to PostHog after `next build` and before the
* web bundle is copied into the Electron package. Sourced from
* `POSTHOG_CLI_API_KEY` (or the legacy `POSTHOG_PERSONAL_API_KEY` alias)
* in CI; when missing (local packaging by a contributor, fork builds, PRs)
* the helper still strips the .map files so source never leaks into the
* shipped installer it just skips the upload step.
*/
posthogCliApiKey?: string;
/**
* PostHog project ID (e.g. `420348` for the official Open Design project)
* used by `@posthog/cli sourcemap upload`. Sourced from
* `POSTHOG_CLI_PROJECT_ID` (or the alias `POSTHOG_PROJECT_ID`) in CI.
* Required for upload to be attempted; missing strip-only path.
*/
posthogCliProjectId?: string;
/**
* PostHog **management** host used by `@posthog/cli sourcemap upload`. This
* is the regional app host (e.g. `https://us.posthog.com`) distinct from
* `posthogHost` above, which is the **ingest** host (`us.i.posthog.com`)
* used by the runtime SDK and accepts `/capture/` traffic only. Sourced
* from `POSTHOG_CLI_HOST`; when missing, the CLI defaults to the US Cloud
* app host on its own, which is correct for the official project.
*/
posthogCliHost?: string;
to: ToolPackBuildOutput;
webOutputMode: ToolPackWebOutputMode;
workspaceRoot: string;
@ -151,6 +177,46 @@ function resolveToolPackPosthogHost(value: string | undefined): string | undefin
return normalized.replace(/\/+$/, "");
}
function resolveToolPackPosthogCliApiKey(value: string | undefined): string | undefined {
if (value == null) return undefined;
const normalized = value.trim();
if (normalized.length === 0) return undefined;
// Personal API keys start with `phx_`. As with POSTHOG_KEY, third-party
// PostHog deployments may use different prefixes; only flag obviously-wrong
// values (whitespace, control chars) so a misconfigured CI secret doesn't
// silently corrupt the upload step.
if (/[\s\x00-\x1f]/.test(normalized)) {
throw new Error(`POSTHOG_CLI_API_KEY contains whitespace or control chars`);
}
return normalized;
}
function resolveToolPackPosthogCliProjectId(value: string | undefined): string | undefined {
if (value == null) return undefined;
const normalized = value.trim();
if (normalized.length === 0) return undefined;
if (!/^[0-9]+$/.test(normalized)) {
throw new Error(`POSTHOG_CLI_PROJECT_ID must be a numeric project id: ${value}`);
}
return normalized;
}
function resolveToolPackPosthogCliHost(value: string | undefined): string | undefined {
if (value == null) return undefined;
const normalized = value.trim();
if (normalized.length === 0) return undefined;
let parsed: URL;
try {
parsed = new URL(normalized);
} catch {
throw new Error(`POSTHOG_CLI_HOST must be an absolute URL: ${value}`);
}
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
throw new Error(`POSTHOG_CLI_HOST must be http(s): ${value}`);
}
return normalized.replace(/\/+$/, "");
}
function resolveToolPackTelemetryRelayUrl(value: string | undefined): string | undefined {
if (value == null) return undefined;
const normalized = value.trim();
@ -239,6 +305,13 @@ export function resolveToolPackConfig(
telemetryRelayUrl: resolveToolPackTelemetryRelayUrl(process.env.OPEN_DESIGN_TELEMETRY_RELAY_URL),
posthogKey: resolveToolPackPosthogKey(process.env.POSTHOG_KEY),
posthogHost: resolveToolPackPosthogHost(process.env.POSTHOG_HOST),
posthogCliApiKey: resolveToolPackPosthogCliApiKey(
process.env.POSTHOG_CLI_API_KEY ?? process.env.POSTHOG_PERSONAL_API_KEY,
),
posthogCliProjectId: resolveToolPackPosthogCliProjectId(
process.env.POSTHOG_CLI_PROJECT_ID ?? process.env.POSTHOG_PROJECT_ID,
),
posthogCliHost: resolveToolPackPosthogCliHost(process.env.POSTHOG_CLI_HOST),
to: resolveToolPackBuildOutput(platform, options.to),
webOutputMode: resolveToolPackWebOutputMode(platform, process.env.OD_WEB_OUTPUT_MODE),
workspaceRoot: WORKSPACE_ROOT,

View file

@ -29,6 +29,7 @@ import {
import type { ToolPackConfig } from "./config.js";
import { copyBundledResourceTrees, linuxResources } from "./resources.js";
import { electronBuilderVersionForAppVersion, readRuntimeAppVersion } from "./versions.js";
import { processWebSourcemaps } from "./web-sourcemaps.js";
const execFileAsync = promisify(execFile);
@ -377,6 +378,10 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise<void> {
try {
await runPnpm(config, ["--filter", "@open-design/web", "build"], { OD_WEB_OUTPUT_MODE: "server" });
await runPnpm(config, ["--filter", "@open-design/web", "build:sidecar"]);
// Inject chunk IDs + upload browser sourcemaps to PostHog, then strip
// .map files before AppImage packaging. See
// `tools/pack/src/web-sourcemaps.ts`.
await processWebSourcemaps(config);
} finally {
if (previousWebNextEnv == null) {
await rm(webNextEnvPath, { force: true });

View file

@ -3,6 +3,7 @@ import { join } from "node:path";
import type { ToolPackCache } from "../cache.js";
import type { ToolPackConfig } from "../config.js";
import { processWebSourcemaps } from "../web-sourcemaps.js";
import { ensureWorkspaceBuildArtifacts } from "../workspace-build.js";
import { runPnpm } from "./commands.js";
@ -24,6 +25,10 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise<void> {
OD_WEB_OUTPUT_MODE: config.webOutputMode,
});
await runPnpm(config, ["--filter", "@open-design/web", "build:sidecar"]);
// Inject chunk IDs + upload browser sourcemaps to PostHog, then strip
// .map files. Runs before any packaging step copies the web output into
// the Electron resources so .map never ends up inside the .app bundle.
await processWebSourcemaps(config);
} finally {
if (previousWebNextEnv == null) {
await rm(webNextEnvPath, { force: true });

View file

@ -0,0 +1,229 @@
// Browser-sourcemap post-build step for packaged builds.
//
// Why this exists
// ---------------
// `apps/web/next.config.ts` sets `productionBrowserSourceMaps: true`, so
// every `next build` invoked from tools-pack also produces `.js.map` files
// alongside the minified chunks. That gives us two requirements:
//
// 1. Send the maps to PostHog so the Error tracking page can symbolicate
// stack frames (otherwise users see `fO / fz / s4` instead of real
// function names + file:line).
// 2. Make sure no `.map` ever ends up inside a shipped installer (`.dmg`,
// `.nsis`, `.AppImage`). Sourcemaps publish the original TypeScript
// source to anyone who can read the bundle, which is a security &
// competitive-disclosure problem.
//
// `processWebSourcemaps` does both:
//
// - With `POSTHOG_CLI_API_KEY` + `POSTHOG_CLI_PROJECT_ID` (CI on
// release-{beta,stable,preview}): run `@posthog/cli sourcemap inject`
// to bake chunk IDs into the JS/map pair, then `sourcemap upload` to
// ship the maps to PostHog. Best-effort — if upload fails (rate limit,
// network blip), the strip step below still runs.
//
// - Without those env vars (local `pnpm tools-pack mac build` by a
// contributor, fork PR builds): skip both inject and upload, just strip.
//
// Either way, the final step ALWAYS removes every `.map` under the
// browser chunks directory. Stripping is a hard requirement; only the
// upload is conditional.
//
// Scope
// -----
// Only the packaged (mac/win/linux Electron) path is covered here. The OSS
// `od` CLI distribution path serves `apps/web/out/_next/static/chunks/`
// directly and is not currently used by any release artifact; it can be
// added later if the OSS audience reports symbolication needs.
import { existsSync } from "node:fs";
import { readdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { createPackageManagerInvocation } from "@open-design/platform";
import type { ToolPackConfig } from "./config.js";
import { execFileAsync } from "./mac/commands.js";
const POSTHOG_CLI_VERSION = "0.7.11";
const RELEASE_NAME = "open-design-web";
export interface WebSourcemapOptions {
/**
* Optional release version to associate with the uploaded chunks. Falls
* back to `config.appVersion` when omitted; if neither is set the CLI
* derives one from git, which is fine but less precise than passing a
* real semver/nightly identifier from the release workflow.
*/
releaseVersion?: string;
}
interface SourcemapCliEnv {
apiKey: string;
projectId: string;
host?: string;
}
function resolveBrowserChunksDir(workspaceRoot: string): string {
// Both `output: 'standalone'` (mac/win) and the implicit server output
// (linux) write browser chunks to `.next/static`. Static-export mode
// (`apps/web/out/_next/static`) is not used by any release artifact.
return join(workspaceRoot, "apps", "web", ".next", "static");
}
async function findMapFiles(dir: string): Promise<string[]> {
const out: string[] = [];
const stack: string[] = [dir];
while (stack.length > 0) {
const current = stack.pop();
if (current == null) break;
let entries;
try {
entries = await readdir(current, { withFileTypes: true });
} catch {
// Directory might not exist on this branch of the tree; skip silently.
continue;
}
for (const entry of entries) {
const entryPath = join(current, entry.name);
if (entry.isDirectory()) {
stack.push(entryPath);
} else if (entry.isFile() && entry.name.endsWith(".map")) {
out.push(entryPath);
}
}
}
return out;
}
async function deleteMapFiles(dir: string): Promise<number> {
const maps = await findMapFiles(dir);
for (const mapPath of maps) {
await rm(mapPath, { force: true });
}
return maps.length;
}
function readUploadEnv(config: ToolPackConfig): SourcemapCliEnv | null {
if (config.posthogCliApiKey == null || config.posthogCliApiKey.length === 0) return null;
if (config.posthogCliProjectId == null || config.posthogCliProjectId.length === 0) return null;
return {
apiKey: config.posthogCliApiKey,
projectId: config.posthogCliProjectId,
// Deliberately uses `posthogCliHost` (management host, us.posthog.com)
// rather than `posthogHost` (ingest host, us.i.posthog.com). When the
// CLI host is unset the @posthog/cli defaults to the US app host on
// its own, which is correct for the official project.
host: config.posthogCliHost,
};
}
function log(line: string): void {
process.stderr.write(`[web-sourcemaps] ${line}\n`);
}
async function runPnpm(
config: ToolPackConfig,
args: string[],
extraEnv: NodeJS.ProcessEnv = {},
): Promise<void> {
// `createPackageManagerInvocation` is the same primitive every platform's
// local `runPnpm` helper goes through, so the linux containerized build
// (which sets `OD_TOOLS_PACK_PNPM_BIN` to the standalone pnpm binary it
// bootstrapped) picks up the right command here too.
const invocation = createPackageManagerInvocation(args, process.env);
await execFileAsync(invocation.command, invocation.args, {
cwd: config.workspaceRoot,
env: { ...process.env, ...extraEnv },
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
});
}
export async function processWebSourcemaps(
config: ToolPackConfig,
options: WebSourcemapOptions = {},
): Promise<void> {
const chunksDir = resolveBrowserChunksDir(config.workspaceRoot);
if (!existsSync(chunksDir)) {
log(`browser chunks dir not found at ${chunksDir}; skipping`);
return;
}
const initialMaps = await findMapFiles(chunksDir);
if (initialMaps.length === 0) {
log(`no .map files under ${chunksDir}; nothing to do`);
return;
}
log(`found ${initialMaps.length} .map file(s) under ${chunksDir}`);
const uploadEnv = readUploadEnv(config);
const releaseVersion = options.releaseVersion ?? config.appVersion;
if (uploadEnv != null) {
const cliEnv: NodeJS.ProcessEnv = {
...process.env,
POSTHOG_CLI_API_KEY: uploadEnv.apiKey,
POSTHOG_CLI_PROJECT_ID: uploadEnv.projectId,
...(uploadEnv.host ? { POSTHOG_CLI_HOST: uploadEnv.host } : {}),
};
const releaseArgs = [
"--release-name",
RELEASE_NAME,
...(releaseVersion ? ["--release-version", releaseVersion] : []),
];
// inject must succeed first so the chunk ID baked into .js matches the
// .map's metadata; the resulting .js change is just a trailing
// `//# chunkId=...` comment so it's safe to retry the build with the
// same source if anything below fails.
try {
await runPnpm(
config,
[
"dlx",
`@posthog/cli@${POSTHOG_CLI_VERSION}`,
"sourcemap",
"inject",
"--directory",
chunksDir,
...releaseArgs,
],
cliEnv,
);
} catch (error) {
log(`inject failed: ${(error as Error).message}; continuing to strip`);
}
// upload is best-effort — `--no-fail` keeps non-zero exits inside the
// CLI from killing the release. If this fails the user simply sees
// unsymbolicated stacks in PostHog, which is no worse than today.
try {
const hostFlag = uploadEnv.host ? ["--host", uploadEnv.host] : [];
await runPnpm(
config,
[
"dlx",
`@posthog/cli@${POSTHOG_CLI_VERSION}`,
...hostFlag,
"--no-fail",
"sourcemap",
"upload",
"--directory",
chunksDir,
...releaseArgs,
],
cliEnv,
);
} catch (error) {
log(`upload failed: ${(error as Error).message}; continuing to strip`);
}
} else {
log("POSTHOG_CLI_API_KEY/POSTHOG_CLI_PROJECT_ID missing; skipping upload");
}
// Hard requirement: never let a .map slip into the shipped installer.
// This runs even if the CLI's own `--delete-after` succeeded — duplicate
// delete is a no-op, and the explicit pass also catches files the CLI
// skipped (anything that wasn't paired with a matching .js, or .map
// files added by future tooling we haven't audited yet).
const stripped = await deleteMapFiles(chunksDir);
log(`stripped ${stripped} .map file(s) before packaging`);
}

View file

@ -27,6 +27,7 @@ import {
shouldInstallInternalPackageForWinPrebundle,
shouldUseWinStandalonePrebundle,
} from "../win-prebundle.js";
import { processWebSourcemaps } from "../web-sourcemaps.js";
import { ensureWorkspaceBuildArtifacts } from "../workspace-build.js";
import {
ELECTRON_BUILDER_BUILD_DEPENDENCIES_FROM_SOURCE,
@ -128,6 +129,10 @@ async function buildWorkspaceArtifacts(config: ToolPackConfig): Promise<void> {
try {
await runPnpm(config, ["--filter", "@open-design/web", "build"], { OD_WEB_OUTPUT_MODE: config.webOutputMode });
await runPnpm(config, ["--filter", "@open-design/web", "build:sidecar"]);
// Inject chunk IDs + upload browser sourcemaps to PostHog, then strip
// .map files before any packaging step copies the web output into the
// Electron resources. See `tools/pack/src/web-sourcemaps.ts`.
await processWebSourcemaps(config);
} finally {
if (previousWebNextEnv == null) await rm(webNextEnvPath, { force: true });
else await writeFile(webNextEnvPath, previousWebNextEnv, "utf8");

View file

@ -0,0 +1,165 @@
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,
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("/*");
});
});