Ctrl K
ComunidadGuías…

frontend/test/browser-symbols.test.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// @vitest-environment node
import { execFileSync } from 'node:child_process';
import { mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve } from 'node:path';
import { SentryCli } from '@sentry/cli';
import { build } from 'vite';
import { afterEach, expect, it } from 'vitest';
import { browserSymbols } from '../scripts/sentry-build';
import { prepareBrowserUpload } from '../scripts/upload-sourcemaps';

const fixtures: string[] = [];
afterEach(() => {
  for (const fixture of fixtures.splice(0)) {
    if (!fixture.startsWith(realpathSync(tmpdir()))) throw new Error('临时目录必须属于系统临时目录');
    rmSync(fixture, { recursive: true, force: true });
  }
});

it('官方构建生成可复现 Debug ID,私有 map 可还原源码位置,公开目录只含正式文件与本次的 chunk', async () => {
  const root = realpathSync(mkdtempSync(resolve(tmpdir(), 'peach-symbols-')));
  fixtures.push(root);
  const directory = resolve(root, 'artifacts');
  const shipped = resolve(root, 'public');
  writeFileSync(resolve(root, 'fixture.ts'), 'import "./fixture.css";\nexport function fail() { throw new TypeError("symbolication fixture"); }\n'
    + 'export const later = () => import("./later.ts");\n');
  writeFileSync(resolve(root, 'later.ts'), 'export const value = "later chunk";\n');
  writeFileSync(resolve(root, 'fixture.css'), '.fixture { color: red; }\n');
  const compile = async (name: 'peach-app' | 'peach-pages') => {
    await build({
      // 公开目录就叫 `public`,不关掉 publicDir 的话 Vite 会把它整个抄回私有目录。
      configFile: false, root, logLevel: 'silent', publicDir: false,
      plugins: browserSymbols(name, shipped),
      build: {
        outDir: directory, emptyOutDir: false, sourcemap: 'hidden', minify: 'oxc',
        lib: { entry: resolve(root, 'fixture.ts'), formats: ['es'], fileName: () => `${name}.js`, cssFileName: name },
        rollupOptions: { output: {
          assetFileNames: `${name}.[ext]`, chunkFileNames: `${name}.[name]-[hash].js`,
          ...(name === 'peach-pages' ? { codeSplitting: false } : {}),
        } },
      },
    });
  };
  await compile('peach-app');
  const first = readFileSync(resolve(shipped, 'peach-app.js'), 'utf8');
  const chunk = readdirSync(shipped).find(name => /^peach-app\.later-[\w-]+\.js$/.test(name));
  expect(chunk).toBeDefined();
  expect(first).toContain(`import("./${chunk}")`);
  writeFileSync(resolve(shipped, 'peach-app.stale-Old00000.js'), 'previous build');
  await compile('peach-app');
  expect(readFileSync(resolve(shipped, 'peach-app.js'), 'utf8')).toBe(first);
  await compile('peach-pages');
  expect(readdirSync(shipped).sort()).toEqual(['peach-app.css', 'peach-app.js', chunk, 'peach-pages.css', 'peach-pages.js']);
  expect(first).not.toContain('sourceMappingURL');
  const mapPath = resolve(directory, 'peach-app.js.map');
  const map = JSON.parse(readFileSync(mapPath, 'utf8'));
  expect(map.sourceRoot).toBeUndefined();
  expect(map.sources.every((source: string) => !/^(?:[a-z]:|\/)/i.test(source))).toBe(true);
  expect(map.debug_id).toMatch(/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/);
  expect(first).toContain(`//# debugId=${map.debug_id}`);
  const token = first.indexOf('TypeError("symbolication fixture")');
  expect(token).toBeGreaterThan(0);
  const before = first.slice(0, token).split('\n');
  const output = execFileSync(SentryCli.getPath(), [
    'sourcemaps', 'resolve', mapPath, '--line', String(before.length), '--column', String(before.at(-1)!.length),
  ], { encoding: 'utf8' });
  expect(output).toContain('fixture.ts');
  expect(output).toContain('symbolication fixture');
  const env = { SENTRY_AUTH_TOKEN: 'test-only', SENTRY_ORG: 'fixture', SENTRY_PROJECT: 'fixture' };
  expect(prepareBrowserUpload(env, directory, shipped)).toEqual([
    'sourcemaps', 'upload', '--validate', '--strict', '--wait', '--url-prefix', 'app:///', directory,
  ]);
  writeFileSync(resolve(shipped, 'peach-app.js'), 'mismatched bundle');
  expect(() => prepareBrowserUpload(env, directory, shipped)).toThrow('分发产物不一致');
  writeFileSync(resolve(shipped, 'peach-app.js'), first);
  writeFileSync(mapPath, JSON.stringify({ ...map, debug_id: 'invalid' }));
  expect(() => prepareBrowserUpload(env, directory, shipped)).toThrow('匹配的 source map');
  // 这条用例在测试里跑一整遍正式构建:拆出 chunk 后每个都要核对 Debug ID,Linux runner 上实测 45 秒。
}, 120_000);

it('上传入口在没有明确环境配置时拒绝启动 CLI', () => {
  expect(() => prepareBrowserUpload({})).toThrow('SENTRY_AUTH_TOKEN');
  expect(() => prepareBrowserUpload({ SENTRY_AUTH_TOKEN: 'test-only' })).toThrow('SENTRY_ORG');
  expect(() => prepareBrowserUpload({ SENTRY_AUTH_TOKEN: 'test-only', SENTRY_ORG: 'fixture' })).toThrow('SENTRY_PROJECT');
});