mirror of
https://github.com/nexu-io/open-design.git
synced 2026-07-09 16:35:54 +08:00
* i18n: add translations for media provider coming soon section (#2415) * i18n: add translations for media provider coming soon section - Add 'settings.mediaProviderComingSoonHint' key to all 19 locales - Replace hardcoded English strings in SettingsDialog.tsx with i18n keys - Reuse existing 'tasks.comingSoon' and 'settings.agentInstall.docs' keys - Resolves TODO(i18n) comment at line 5091 * fix: escape single quotes in translation strings * fix: escape all single quotes in English translation string * feat(release): upload browser sourcemaps to PostHog for packaged builds Next.js was emitting minified JS with no browser sourcemaps, so PostHog Error Tracking surfaces frames like fO / fz / s4 / tD instead of real file:line locations. This wires up the full pipeline: - apps/web/next.config.ts: enable productionBrowserSourceMaps so next build emits .js.map alongside each chunk. - tools/pack/src/web-sourcemaps.ts: new helper that runs after next build and before any packaging step copies the web output into the Electron resources. Uses @posthog/cli to inject chunk IDs and upload sourcemaps to PostHog, then ALWAYS strips every .map under .next/static so source never ships inside an installer (saves ~14 MB per packaged image too). - tools/pack/src/{mac/workspace,win/app,linux}.ts: call processWebSourcemaps immediately after the @open-design/web build step. - tools/pack/src/config.ts: read POSTHOG_CLI_API_KEY + POSTHOG_CLI_PROJECT_ID (with POSTHOG_PERSONAL_API_KEY / POSTHOG_PROJECT_ID aliases) and expose them on ToolPackConfig with the same shape as the existing posthogKey / posthogHost fields. - .github/workflows/release-{beta,preview,stable}.yml: pass the new secrets through so all three release channels symbolicate stacks. When the API key is missing (PR builds, forks, local contributor builds), the helper logs and skips the upload — but still strips .map files. The strip step is unconditional because shipping a sourcemap is equivalent to shipping the source. Adds tools/pack/tests/web-sourcemaps.test.ts covering: missing chunks dir silently noop, no-map noop, strip-only path when credentials are absent, recursive walker for nested subdirectories. CLI happy path is left to the release workflow itself. Required follow-up (cannot push from code): add a repo secret named POSTHOG_CLI_API_KEY (the phx_ personal API key) and a repo var named POSTHOG_CLI_PROJECT_ID (the numeric project id, 420348 for our project) in nexu-io/open-design settings before merging. * fix(web-sourcemaps): use management host for CLI, not ingest host POSTHOG_HOST is the ingest URL (us.i.posthog.com) used by the runtime SDK to POST events to /capture/. The @posthog/cli sourcemap upload talks to the **management** API (us.posthog.com) and gets a 404 on the ingest host. The two are not interchangeable. Adds a separate `posthogCliHost` field on ToolPackConfig sourced from POSTHOG_CLI_HOST (with no fallback to POSTHOG_HOST). When the env is unset the @posthog/cli defaults to the US Cloud app host on its own, which is correct for our project — so this PR doesn't need a new repo variable for it. --------- Co-authored-by: Nicholas-Xiong <2482929840@qq.com>
166 lines
6.1 KiB
TypeScript
166 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,
|
|
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("/*");
|
|
});
|
|
});
|