mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
Every image, the standalone tarballs and the Lite zips are built with source maps (`BULWARK_SOURCE_MAPS=1`). Turbopack builds give each chunk a debug ID themselves; the webpack build in the Docker image gets them from `sentry-cli sourcemaps inject`. `scripts/sourcemaps.mjs collect` then moves every map out of the build before anything is packed, and `scripts/sourcemaps.mjs check` fails the build when a `.map` file, an inline map or a `sourceMappingURL` to a missing file would ship. - Docker images: a `sourcemaps` stage holds only the maps; the amd64 job exports it (a cache hit) and uploads it, since both platforms build identical chunks - standalone tarballs, the Lite zips and the Lite image upload their own maps and check what they ship - `scripts/upload-sourcemaps.sh` uploads to `bulwark-instances` and `bulwark-telemetry` (Bugsink keeps maps per project) and skips with a notice when the `BUGSINK_AUTH_TOKEN` secret is missing
97 lines
4.2 KiB
TypeScript
97 lines
4.2 KiB
TypeScript
// @vitest-environment node
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { gzipSync } from 'node:zlib';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
// @ts-expect-error plain .mjs without types
|
|
import { check, collect, mapReferences } from '../../scripts/sourcemaps.mjs';
|
|
// @ts-expect-error plain .mjs without types
|
|
import { buildZip } from '../../scripts/lite/lib.mjs';
|
|
|
|
// CI builds source maps for the error tracker; none may ship (images, the
|
|
// standalone tarball, Lite zips).
|
|
|
|
let dir: string;
|
|
const put = (rel: string, text: string) => {
|
|
const file = path.join(dir, rel);
|
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
writeFileSync(file, text);
|
|
};
|
|
|
|
beforeEach(() => { dir = mkdtempSync(path.join(tmpdir(), 'bulwark-maps-')); });
|
|
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
|
|
|
function tar(files: Record<string, string>): Buffer {
|
|
const blocks: Buffer[] = [];
|
|
for (const [name, text] of Object.entries(files)) {
|
|
const data = Buffer.from(text);
|
|
const header = Buffer.alloc(512);
|
|
header.write(name, 0);
|
|
header.write('0000644\0', 100);
|
|
header.write(data.length.toString(8).padStart(11, '0') + '\0', 124);
|
|
header.write('0', 156);
|
|
header.write('ustar\0', 257);
|
|
blocks.push(header, data, Buffer.alloc((512 - (data.length % 512)) % 512));
|
|
}
|
|
blocks.push(Buffer.alloc(1024));
|
|
return Buffer.concat(blocks);
|
|
}
|
|
|
|
describe('collect', () => {
|
|
it('moves maps out with the chunks that name them, and strips the comment from what ships', () => {
|
|
put('static/chunks/a.js', 'console.log(1);\n//# debugId=bc67f7db-7f3b-7be2-6f94-966cd7825cc2\n//# sourceMappingURL=a1b2.js.map\n');
|
|
put('static/chunks/a1b2.js.map', '{"version":3}');
|
|
put('static/css/x.css', 'a{}\n/*# sourceMappingURL=x.css.map */\n');
|
|
put('static/css/x.css.map', '{"version":3}');
|
|
put('static/chunks/orphan.js.map', '{"version":3}');
|
|
const out = path.join(dir, 'maps');
|
|
|
|
const moved = collect([path.join(dir, 'static')], out);
|
|
|
|
expect(moved).toHaveLength(3);
|
|
expect(existsSync(path.join(dir, 'static/chunks/a1b2.js.map'))).toBe(false);
|
|
const shipped = readFileSync(path.join(dir, 'static/chunks/a.js'), 'utf8');
|
|
expect(mapReferences(shipped)).toEqual([]);
|
|
expect(shipped).toContain('//# debugId=bc67f7db-7f3b-7be2-6f94-966cd7825cc2');
|
|
expect(readFileSync(path.join(out, '0/chunks/a.js'), 'utf8')).toContain('sourceMappingURL=a1b2.js.map');
|
|
expect(existsSync(path.join(out, '0/chunks/a1b2.js.map'))).toBe(true);
|
|
expect(existsSync(path.join(out, '0/chunks/orphan.js.map'))).toBe(true);
|
|
expect(check(path.join(dir, 'static'))).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('check', () => {
|
|
it('finds map files, dangling references and inline maps in a folder', () => {
|
|
put('a/x.js.map', '{}');
|
|
put('a/y.js', 'x\n//# sourceMappingURL=gone.js.map\n');
|
|
put('a/z.js', 'x\n//# sourceMappingURL=data:application/json;base64,e30=\n');
|
|
put('a/ok.js', 'const s = "//# sourceMappingURL=in-a-string.map";\n');
|
|
expect(check(path.join(dir, 'a')).sort()).toEqual([
|
|
'x.js.map: source map file',
|
|
'y.js: sourceMappingURL to missing gone.js.map',
|
|
'z.js: inline source map',
|
|
]);
|
|
});
|
|
|
|
it('reads zip archives', () => {
|
|
const zip = path.join(dir, 'lite.zip');
|
|
writeFileSync(zip, buildZip([
|
|
{ name: '_next/static/chunks/a.js', data: Buffer.from('x\n//# sourceMappingURL=a.js.map\n') },
|
|
{ name: '_next/static/chunks/a.js.map', data: Buffer.from('{}') },
|
|
]));
|
|
expect(check(zip)).toEqual(['_next/static/chunks/a.js.map: source map file']);
|
|
writeFileSync(zip, buildZip([{ name: 'index.html', data: Buffer.from('<html>') }]));
|
|
expect(check(zip)).toEqual([]);
|
|
});
|
|
|
|
it('reads gzipped tarballs', () => {
|
|
const tgz = path.join(dir, 'standalone.tar.gz');
|
|
writeFileSync(tgz, gzipSync(tar({
|
|
'bulwark-standalone/.next/server/chunks/1.js': 'x\n//# sourceMappingURL=1.js.map\n',
|
|
'bulwark-standalone/.next/server/chunks/1.js.map': '{}',
|
|
})));
|
|
expect(check(tgz)).toEqual(['bulwark-standalone/.next/server/chunks/1.js.map: source map file']);
|
|
});
|
|
});
|