mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
feat(release): upload browser sourcemaps to PostHog for packaged builds (#2508)
* 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>
This commit is contained in:
parent
f5f8937421
commit
ebf4a3ffca
31 changed files with 531 additions and 4 deletions
7
.github/workflows/release-beta.yml
vendored
7
.github/workflows/release-beta.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
7
.github/workflows/release-preview.yml
vendored
7
.github/workflows/release-preview.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
7
.github/workflows/release-stable.yml
vendored
7
.github/workflows/release-stable.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5131,15 +5131,14 @@ function MediaProvidersSection({
|
|||
<details className="library-group media-provider-coming-soon">
|
||||
<summary className="memory-details-summary">
|
||||
<span className="memory-details-title">
|
||||
Coming soon
|
||||
{t('tasks.comingSoon')}
|
||||
</span>
|
||||
<span className="filter-pill-count">
|
||||
{comingSoonProviders.length}
|
||||
</span>
|
||||
</summary>
|
||||
<p className="hint" style={{ marginTop: 4, marginBottom: 8 }}>
|
||||
We track these for the roadmap; the daemon doesn’t ship a
|
||||
client yet, so there’s nothing to configure.
|
||||
{t('settings.mediaProviderComingSoonHint')}
|
||||
</p>
|
||||
<ul className="media-provider-coming-soon-list">
|
||||
{comingSoonProviders.map((provider) => {
|
||||
|
|
@ -5164,7 +5163,7 @@ function MediaProvidersSection({
|
|||
rel="noopener noreferrer"
|
||||
className="ghost-link"
|
||||
>
|
||||
Docs
|
||||
{t('settings.agentInstall.docs')}
|
||||
<Icon name="external-link" size={11} />
|
||||
</a>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const ar: Dict = {
|
|||
'settings.mediaProviderReloadError': 'تعذر إعادة تحميل إعدادات موفري الوسائط من الـ daemon المحلي.',
|
||||
'settings.mediaProviderReloadSuccess': 'تمت إعادة تحميل إعدادات موفري الوسائط من الـ daemon المحلي.',
|
||||
'settings.mediaProviderLoadError': 'تعذر تحميل إعدادات موفري الوسائط من الـ daemon المحلي. سيُستخدم مؤقتًا ما هو محفوظ في المتصفح.',
|
||||
'settings.mediaProviderComingSoonHint': 'نحن نتتبع هذه المزودات في خارطة الطريق؛ لم يقم البرنامج الخفي بشحن عميل بعد، لذا لا يوجد شيء لتكوينه.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const de: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Die Einstellungen der Medienanbieter konnten nicht vom lokalen Daemon neu geladen werden.',
|
||||
'settings.mediaProviderReloadSuccess': 'Die Einstellungen der Medienanbieter wurden vom lokalen Daemon neu geladen.',
|
||||
'settings.mediaProviderLoadError': 'Die Einstellungen der Medienanbieter konnten nicht vom lokalen Daemon geladen werden. Vorerst werden die im Browser gespeicherten Einstellungen verwendet.',
|
||||
'settings.mediaProviderComingSoonHint': 'Wir verfolgen diese für die Roadmap; der Daemon liefert noch keinen Client, daher gibt es nichts zu konfigurieren.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ export const en: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Could not reload media provider settings from the local daemon.',
|
||||
'settings.mediaProviderReloadSuccess': 'Reloaded media provider settings from the local daemon.',
|
||||
'settings.mediaProviderLoadError': 'Could not load media provider settings from the local daemon. Using browser-saved settings for now.',
|
||||
'settings.mediaProviderComingSoonHint': 'We track these for the roadmap; the daemon doesn\'t ship a client yet, so there\'s nothing to configure.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const esES: Dict = {
|
|||
'settings.mediaProviderReloadError': 'No se pudieron recargar los ajustes de los proveedores de medios desde el daemon local.',
|
||||
'settings.mediaProviderReloadSuccess': 'Se recargaron los ajustes de los proveedores de medios desde el daemon local.',
|
||||
'settings.mediaProviderLoadError': 'No se pudieron cargar los ajustes de los proveedores de medios desde el daemon local. Por ahora se usarán los ajustes guardados en el navegador.',
|
||||
'settings.mediaProviderComingSoonHint': 'Rastreamos estos para la hoja de ruta; el daemon aún no incluye un cliente, por lo que no hay nada que configurar.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const fa: Dict = {
|
|||
'settings.mediaProviderReloadError': 'بارگذاری دوبارهٔ تنظیمات ارائهدهندههای رسانه از دیمن محلی ممکن نشد.',
|
||||
'settings.mediaProviderReloadSuccess': 'تنظیمات ارائهدهندههای رسانه از دیمن محلی دوباره بارگذاری شد.',
|
||||
'settings.mediaProviderLoadError': 'بارگذاری تنظیمات ارائهدهندههای رسانه از دیمن محلی ممکن نشد. فعلاً از تنظیمات ذخیرهشده در مرورگر استفاده میشود.',
|
||||
'settings.mediaProviderComingSoonHint': 'ما این موارد را برای نقشه راه پیگیری میکنیم؛ دیمون هنوز کلاینتی ارائه نمیدهد، بنابراین چیزی برای پیکربندی وجود ندارد.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const fr: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Impossible de recharger les paramètres des fournisseurs de médias depuis le daemon local.',
|
||||
'settings.mediaProviderReloadSuccess': 'Paramètres des fournisseurs de médias rechargés depuis le daemon local.',
|
||||
'settings.mediaProviderLoadError': 'Impossible de charger les paramètres des fournisseurs de médias depuis le daemon local. Utilisation temporaire des paramètres enregistrés dans le navigateur.',
|
||||
'settings.mediaProviderComingSoonHint': 'Nous suivons ces fournisseurs pour la feuille de route ; le daemon ne fournit pas encore de client, il n\'y a donc rien à configurer.',
|
||||
'settings.privacy': 'Confidentialité',
|
||||
'settings.privacyHint': 'Données partagées avec l’équipe Open Design',
|
||||
'settings.privacyConsentKicker': 'Aidez-nous à améliorer Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const hu: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Nem sikerült újratölteni a médiaszolgáltatók beállításait a helyi démonból.',
|
||||
'settings.mediaProviderReloadSuccess': 'A médiaszolgáltatók beállításai újra lettek töltve a helyi démonból.',
|
||||
'settings.mediaProviderLoadError': 'Nem sikerült betölteni a médiaszolgáltatók beállításait a helyi démonból. Egyelőre a böngészőben mentett beállításokat használjuk.',
|
||||
'settings.mediaProviderComingSoonHint': 'Ezeket nyomon követjük az ütemtervben; a daemon még nem szállít klienst, így nincs mit konfigurálni.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -288,6 +288,7 @@ export const id: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Tidak dapat memuat ulang pengaturan penyedia media dari daemon lokal.',
|
||||
'settings.mediaProviderReloadSuccess': 'Pengaturan penyedia media berhasil dimuat ulang dari daemon lokal.',
|
||||
'settings.mediaProviderLoadError': 'Tidak dapat memuat pengaturan penyedia media dari daemon lokal. Untuk sementara menggunakan pengaturan yang tersimpan di browser.',
|
||||
'settings.mediaProviderComingSoonHint': 'Kami melacak ini untuk roadmap; daemon belum mengirimkan klien, jadi tidak ada yang perlu dikonfigurasi.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ export const it: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Impossibile ricaricare le impostazioni dei provider di media dal daemon locale.',
|
||||
'settings.mediaProviderReloadSuccess': 'Impostazioni dei provider di media ricaricate dal daemon locale.',
|
||||
'settings.mediaProviderLoadError': 'Impossibile caricare le impostazioni dei provider di media dal daemon locale. Uso temporaneo delle impostazioni salvate nel browser.',
|
||||
'settings.mediaProviderComingSoonHint': 'Teniamo traccia di questi per la roadmap; il daemon non fornisce ancora un client, quindi non c\'è nulla da configurare.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'Quali dati vengono condivisi con il team di Open Design',
|
||||
'settings.privacyConsentKicker': 'Aiutaci a migliorare Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const ja: Dict = {
|
|||
'settings.mediaProviderReloadError': 'ローカルデーモンからメディアプロバイダー設定を再読み込みできませんでした。',
|
||||
'settings.mediaProviderReloadSuccess': 'ローカルデーモンからメディアプロバイダー設定を再読み込みしました。',
|
||||
'settings.mediaProviderLoadError': 'ローカルデーモンからメディアプロバイダー設定を読み込めませんでした。今のところブラウザーに保存された設定を使用します。',
|
||||
'settings.mediaProviderComingSoonHint': 'これらはロードマップで追跡しています。デーモンはまだクライアントを提供していないため、設定するものはありません。',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const ko: Dict = {
|
|||
'settings.mediaProviderReloadError': '로컬 데몬에서 미디어 제공자 설정을 다시 불러오지 못했습니다.',
|
||||
'settings.mediaProviderReloadSuccess': '로컬 데몬에서 미디어 제공자 설정을 다시 불러왔습니다.',
|
||||
'settings.mediaProviderLoadError': '로컬 데몬에서 미디어 제공자 설정을 불러오지 못했습니다. 지금은 브라우저에 저장된 설정을 사용합니다.',
|
||||
'settings.mediaProviderComingSoonHint': '로드맵에서 이를 추적하고 있습니다. 데몬이 아직 클라이언트를 제공하지 않으므로 구성할 항목이 없습니다.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -293,6 +293,7 @@ export const pl: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Nie udało się ponownie wczytać ustawień dostawców mediów z lokalnego demona.',
|
||||
'settings.mediaProviderReloadSuccess': 'Ustawienia dostawców mediów zostały ponownie wczytane z lokalnego demona.',
|
||||
'settings.mediaProviderLoadError': 'Nie udało się wczytać ustawień dostawców mediów z lokalnego demona. Na razie używane będą ustawienia zapisane w przeglądarce.',
|
||||
'settings.mediaProviderComingSoonHint': 'Śledzimy je w mapie drogowej; daemon nie dostarcza jeszcze klienta, więc nie ma nic do skonfigurowania.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ export const ptBR: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Não foi possível recarregar as configurações dos provedores de mídia do daemon local.',
|
||||
'settings.mediaProviderReloadSuccess': 'As configurações dos provedores de mídia foram recarregadas do daemon local.',
|
||||
'settings.mediaProviderLoadError': 'Não foi possível carregar as configurações dos provedores de mídia do daemon local. Usando por enquanto as configurações salvas no navegador.',
|
||||
'settings.mediaProviderComingSoonHint': 'Rastreamos estes para o roteiro; o daemon ainda não fornece um cliente, então não há nada para configurar.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -292,6 +292,7 @@ export const ru: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Не удалось заново загрузить настройки медиапровайдеров из локального демона.',
|
||||
'settings.mediaProviderReloadSuccess': 'Настройки медиапровайдеров заново загружены из локального демона.',
|
||||
'settings.mediaProviderLoadError': 'Не удалось загрузить настройки медиапровайдеров из локального демона. Пока используются настройки, сохранённые в браузере.',
|
||||
'settings.mediaProviderComingSoonHint': 'Мы отслеживаем их в дорожной карте; демон пока не поставляет клиент, поэтому настраивать нечего.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -277,6 +277,7 @@ export const th: Dict = {
|
|||
'settings.mediaProviderClearConfirm': 'ล้างการตั้งค่า {name} ที่บันทึกไว้ใช่หรือไม่? คุณจะต้องตั้งค่าใหม่อีกครั้งเพื่อใช้งาน {name}',
|
||||
'settings.mediaProviderPlaceholder': 'วาง API key',
|
||||
'settings.mediaProviderBaseUrlPlaceholder': 'กำหนด Base URL แท่นค่าเริ่มต้น',
|
||||
'settings.mediaProviderComingSoonHint': 'เราติดตามสิ่งเหล่านี้สำหรับแผนงาน daemon ยังไม่ได้จัดส่งไคลเอนต์ ดังนั้นจึงไม่มีอะไรให้กำหนดค่า',
|
||||
'settings.privacy': 'ความเป็นส่วนตัว',
|
||||
'settings.privacyHint': 'ข้อมูลที่แชร์กับทีม Open Design',
|
||||
'settings.privacyConsentKicker': 'ช่วยเราพัฒนา Open Design',
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@ export const tr: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Medya sağlayıcı ayarları yerel daemon’dan yeniden yüklenemedi.',
|
||||
'settings.mediaProviderReloadSuccess': 'Medya sağlayıcı ayarları yerel daemon’dan yeniden yüklendi.',
|
||||
'settings.mediaProviderLoadError': 'Medya sağlayıcı ayarları yerel daemon’dan yüklenemedi. Şimdilik tarayıcıya kaydedilen ayarlar kullanılıyor.',
|
||||
'settings.mediaProviderComingSoonHint': 'Bunları yol haritası için takip ediyoruz; daemon henüz bir istemci göndermediği için yapılandırılacak bir şey yok.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ export const uk: Dict = {
|
|||
'settings.mediaProviderReloadError': 'Не вдалося повторно завантажити налаштування медіапровайдерів із локального демона.',
|
||||
'settings.mediaProviderReloadSuccess': 'Налаштування медіапровайдерів повторно завантажено з локального демона.',
|
||||
'settings.mediaProviderLoadError': 'Не вдалося завантажити налаштування медіапровайдерів із локального демона. Наразі використовуються налаштування, збережені в браузері.',
|
||||
'settings.mediaProviderComingSoonHint': 'Ми відстежуємо їх у дорожній карті; демон ще не постачає клієнт, тому налаштовувати нічого.',
|
||||
'settings.privacy': 'Privacy',
|
||||
'settings.privacyHint': 'What data is shared with the Open Design team',
|
||||
'settings.privacyConsentKicker': 'Help us improve Open Design',
|
||||
|
|
|
|||
|
|
@ -306,6 +306,7 @@ export const zhCN: Dict = {
|
|||
'settings.mediaProviderReloadError': '无法从本地守护进程重新加载媒体提供方设置。',
|
||||
'settings.mediaProviderReloadSuccess': '已从本地守护进程重新加载媒体提供方设置。',
|
||||
'settings.mediaProviderLoadError': '无法从本地守护进程加载媒体提供方设置。当前将使用浏览器中保存的设置。',
|
||||
'settings.mediaProviderComingSoonHint': '我们在路线图中跟踪这些提供方;守护进程尚未提供客户端,因此暂无可配置项。',
|
||||
'settings.privacy': '隐私',
|
||||
'settings.privacyHint': '与 Open Design 团队共享哪些数据',
|
||||
'settings.privacyConsentKicker': '帮助我们改进 Open Design',
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ export const zhTW: Dict = {
|
|||
'settings.mediaProviderReloadError': '無法從本機守護程序重新載入媒體供應商設定。',
|
||||
'settings.mediaProviderReloadSuccess': '已從本機守護程序重新載入媒體供應商設定。',
|
||||
'settings.mediaProviderLoadError': '無法從本機守護程序載入媒體供應商設定。目前將使用瀏覽器中儲存的設定。',
|
||||
'settings.mediaProviderComingSoonHint': '我們在路線圖中追蹤這些提供者;守護程式尚未提供客戶端,因此暫無可配置項。',
|
||||
'settings.privacy': '隱私',
|
||||
'settings.privacyHint': '與 Open Design 團隊共享哪些資料',
|
||||
'settings.privacyConsentKicker': '協助我們改進 Open Design',
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ export interface Dict {
|
|||
'settings.mediaProviderReloadError': string;
|
||||
'settings.mediaProviderReloadSuccess': string;
|
||||
'settings.mediaProviderLoadError': string;
|
||||
'settings.mediaProviderComingSoonHint': string;
|
||||
'settings.privacy': string;
|
||||
'settings.privacyHint': string;
|
||||
'settings.privacyConsentKicker': string;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
229
tools/pack/src/web-sourcemaps.ts
Normal file
229
tools/pack/src/web-sourcemaps.ts
Normal 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`);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
165
tools/pack/tests/web-sourcemaps.test.ts
Normal file
165
tools/pack/tests/web-sourcemaps.test.ts
Normal 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("/*");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue