Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/validate-selfhost-sourcemap.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type ValidateSourcemapOptions = {
bundlePath?: string;
mapPath?: string;
exists?: (path: string) => boolean;
readFile?: (path: string) => string;
};

export declare function validateSourcemap(options?: ValidateSourcemapOptions): { sourceCount: number };
132 changes: 81 additions & 51 deletions scripts/validate-selfhost-sourcemap.mjs
Original file line number Diff line number Diff line change
@@ -1,63 +1,93 @@
#!/usr/bin/env node
// Validates that the self-host Docker build's dist/server.mjs + dist/server.mjs.map are structurally
// sound and resolve back to real repository source (not an empty/broken map). Gates error.stack
// symbolication for every self-hosted deployment. Invoked from release-selfhost.yml and selfhost.yml.
//
// #7458: validation is an injectable named export so unit tests can cover every failure branch without
// a real dist/ build. The CLI entrypoint below preserves the previous cwd-relative + exit-1 behavior.

import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";

const root = process.cwd();
const bundlePath = resolve(root, "dist/server.mjs");
const mapPath = resolve(root, "dist/server.mjs.map");
/**
* @param {{
* bundlePath?: string,
* mapPath?: string,
* exists?: (path: string) => boolean,
* readFile?: (path: string) => string,
* }} [options]
* @returns {{ sourceCount: number }}
*/
export function validateSourcemap(options = {}) {
const bundlePath = options.bundlePath ?? resolve(process.cwd(), "dist/server.mjs");
const mapPath = options.mapPath ?? resolve(process.cwd(), "dist/server.mjs.map");
const exists = options.exists ?? existsSync;
const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));

function fail(message) {
console.error(`self-host sourcemap validation failed: ${message}`);
process.exit(1);
}
if (!exists(bundlePath)) throw new Error("dist/server.mjs is missing");
if (!exists(mapPath)) throw new Error("dist/server.mjs.map is missing");

if (!existsSync(bundlePath)) fail("dist/server.mjs is missing");
if (!existsSync(mapPath)) fail("dist/server.mjs.map is missing");
const bundle = readFile(bundlePath);
if (!bundle.includes("//# sourceMappingURL=server.mjs.map")) {
throw new Error("dist/server.mjs is missing the server.mjs.map sourceMappingURL");
}

const bundle = readFileSync(bundlePath, "utf8");
if (!bundle.includes("//# sourceMappingURL=server.mjs.map")) {
fail("dist/server.mjs is missing the server.mjs.map sourceMappingURL");
}
let map;
try {
map = JSON.parse(readFile(mapPath));
} catch (error) {
throw new Error(
`dist/server.mjs.map is not valid JSON (${error instanceof Error ? error.message : String(error)})`,
);
}

let map;
try {
map = JSON.parse(readFileSync(mapPath, "utf8"));
} catch (error) {
fail(`dist/server.mjs.map is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
}
if (map.version !== 3) throw new Error("dist/server.mjs.map is not a version 3 source map");
if (!Array.isArray(map.sources) || map.sources.length === 0) {
throw new Error("dist/server.mjs.map has no original sources");
}
if (!Array.isArray(map.sourcesContent) || map.sourcesContent.length !== map.sources.length) {
throw new Error("dist/server.mjs.map must include sourcesContent for every original source");
}
const serverSourceIndex = map.sources.findIndex((source) => String(source).endsWith("src/server.ts"));
if (serverSourceIndex === -1) {
throw new Error("dist/server.mjs.map does not include src/server.ts");
}
if (map.sourcesContent[serverSourceIndex]?.trim() === "") {
throw new Error("dist/server.mjs.map has empty source content for src/server.ts");
}
const repoSourceIndexes = map.sources
.map((source, index) => [String(source), index])
.filter(([source]) => source.startsWith("../src/"))
.map(([, index]) => index);
if (repoSourceIndexes.length === 0) {
throw new Error("dist/server.mjs.map does not include repository sources");
}
if (
repoSourceIndexes.some(
(index) => typeof map.sourcesContent[index] !== "string" || map.sourcesContent[index].trim() === "",
)
) {
throw new Error("dist/server.mjs.map is missing source content for a repository source");
}

if (map.version !== 3) fail("dist/server.mjs.map is not a version 3 source map");
if (!Array.isArray(map.sources) || map.sources.length === 0) {
fail("dist/server.mjs.map has no original sources");
}
if (!Array.isArray(map.sourcesContent) || map.sourcesContent.length !== map.sources.length) {
fail("dist/server.mjs.map must include sourcesContent for every original source");
return { sourceCount: map.sources.length };
}
const serverSourceIndex = map.sources.findIndex((source) =>
String(source).endsWith("src/server.ts"),
);
if (serverSourceIndex === -1) {
fail("dist/server.mjs.map does not include src/server.ts");
}
if (map.sourcesContent[serverSourceIndex]?.trim() === "") {
fail("dist/server.mjs.map has empty source content for src/server.ts");
}
const repoSourceIndexes = map.sources
.map((source, index) => [String(source), index])
.filter(([source]) => source.startsWith("../src/"))
.map(([, index]) => index);
if (repoSourceIndexes.length === 0) {
fail("dist/server.mjs.map does not include repository sources");

function fail(message) {
console.error(`self-host sourcemap validation failed: ${message}`);
process.exit(1);
}
if (
repoSourceIndexes.some(
(index) =>
typeof map.sourcesContent[index] !== "string" ||
map.sourcesContent[index].trim() === "",
)
) {
fail("dist/server.mjs.map is missing source content for a repository source");

function main() {
try {
const { sourceCount } = validateSourcemap();
console.log(`self-host sourcemap validation passed (${sourceCount} original sources)`);
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}

console.log(
`self-host sourcemap validation passed (${map.sources.length} original sources)`,
);
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
main();
}
162 changes: 162 additions & 0 deletions test/unit/validate-selfhost-sourcemap-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import { validateSourcemap } from "../../scripts/validate-selfhost-sourcemap.mjs";

const BUNDLE = "/tmp/dist/server.mjs";
const MAP = "/tmp/dist/server.mjs.map";

const VALID_BUNDLE = "export {};\n//# sourceMappingURL=server.mjs.map\n";

function validMap(overrides: Record<string, unknown> = {}): string {
return JSON.stringify({
version: 3,
sources: ["../src/server.ts"],
sourcesContent: ["export function start() {}\n"],
mappings: "AAAA",
...overrides,
});
}

function harness(files: Record<string, string | undefined>) {
return {
bundlePath: BUNDLE,
mapPath: MAP,
exists: (path: string) => Object.prototype.hasOwnProperty.call(files, path) && files[path] !== undefined,
readFile: (path: string) => {
const content = files[path];
if (content === undefined) throw new Error(`ENOENT: ${path}`);
return content;
},
};
}

describe("validate-selfhost-sourcemap.mjs (#7458)", () => {
it("passes a well-formed minimal source map", () => {
expect(
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap(),
}),
),
).toEqual({ sourceCount: 1 });
});

it("fails when the bundle is missing", () => {
expect(() => validateSourcemap(harness({ [MAP]: validMap() }))).toThrow("dist/server.mjs is missing");
});

it("fails when the map is missing", () => {
expect(() => validateSourcemap(harness({ [BUNDLE]: VALID_BUNDLE }))).toThrow("dist/server.mjs.map is missing");
});

it("fails when the bundle is missing the sourceMappingURL comment", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: "export {};\n",
[MAP]: validMap(),
}),
),
).toThrow("dist/server.mjs is missing the server.mjs.map sourceMappingURL");
});

it("fails when the map is not valid JSON", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: "{ not json",
}),
),
).toThrow(/dist\/server\.mjs\.map is not valid JSON/);
});

it("fails when the map is not version 3", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({ version: 2 }),
}),
),
).toThrow("dist/server.mjs.map is not a version 3 source map");
});

it("fails when sources is empty", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({ sources: [], sourcesContent: [] }),
}),
),
).toThrow("dist/server.mjs.map has no original sources");
});

it("fails when sourcesContent length does not match sources", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({ sourcesContent: [] }),
}),
),
).toThrow("dist/server.mjs.map must include sourcesContent for every original source");
});

it("fails when src/server.ts is missing from sources", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({
sources: ["../src/other.ts"],
sourcesContent: ["export {}\n"],
}),
}),
),
).toThrow("dist/server.mjs.map does not include src/server.ts");
});

it("fails when src/server.ts source content is empty", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({
sources: ["../src/server.ts"],
sourcesContent: [" "],
}),
}),
),
).toThrow("dist/server.mjs.map has empty source content for src/server.ts");
});

it("fails when no repository-relative ../src/ sources are present", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({
sources: ["src/server.ts"],
sourcesContent: ["export function start() {}\n"],
}),
}),
),
).toThrow("dist/server.mjs.map does not include repository sources");
});

it("fails when a repository-relative source has empty content", () => {
expect(() =>
validateSourcemap(
harness({
[BUNDLE]: VALID_BUNDLE,
[MAP]: validMap({
sources: ["../src/server.ts", "../src/other.ts"],
sourcesContent: ["export function start() {}\n", " "],
}),
}),
),
).toThrow("dist/server.mjs.map is missing source content for a repository source");
});
});