Files
bun.sh/test/bundler/compile-windows-metadata.test.ts
Alistair Smith 71ce550cfa esm bytecode (#26402)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-01-30 01:38:45 -08:00

741 lines
21 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { execSync } from "child_process";
import { promises as fs } from "fs";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { join } from "path";
// Helper to ensure executable cleanup
function cleanup(outfile: string) {
return {
[Symbol.asyncDispose]: async () => {
try {
await fs.rm(outfile, { force: true });
} catch {}
},
};
}
describe.skipIf(!isWindows).concurrent("Windows compile metadata", () => {
describe("CLI flags", () => {
test("all metadata flags via CLI", async () => {
using dir = tempDir("windows-metadata-cli", {
"app.js": `console.log("Test app with metadata");`,
});
const outfile = join(String(dir), "app-with-metadata.exe");
await using _cleanup = cleanup(outfile);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"My Application",
"--windows-publisher",
"Test Company Inc",
"--windows-version",
"1.2.3.4",
"--windows-description",
"A test application with metadata",
"--windows-copyright",
"Copyright © 2024 Test Company Inc",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
// Verify executable was created
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
// Verify metadata using PowerShell
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("My Application");
expect(getMetadata("CompanyName")).toBe("Test Company Inc");
expect(getMetadata("FileDescription")).toBe("A test application with metadata");
expect(getMetadata("LegalCopyright")).toBe("Copyright © 2024 Test Company Inc");
expect(getMetadata("ProductVersion")).toBe("1.2.3.4");
expect(getMetadata("FileVersion")).toBe("1.2.3.4");
});
test("partial metadata flags", async () => {
using dir = tempDir("windows-metadata-partial", {
"app.js": `console.log("Partial metadata test");`,
});
const outfile = join(String(dir), "app-partial.exe");
await using _cleanup = cleanup(outfile);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"Simple App",
"--windows-version",
"2.0.0.0",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("Simple App");
expect(getMetadata("ProductVersion")).toBe("2.0.0.0");
expect(getMetadata("FileVersion")).toBe("2.0.0.0");
});
test("windows flags without --compile should error", async () => {
using dir = tempDir("windows-no-compile", {
"app.js": `console.log("test");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", join(String(dir), "app.js"), "--windows-title", "Should Fail"],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("--windows-title requires --compile");
});
test("windows flags with non-Windows target should error", async () => {
using dir = tempDir("windows-wrong-target", {
"app.js": `console.log("test");`,
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
"--target",
"bun-linux-x64",
join(String(dir), "app.js"),
"--windows-title",
"Should Fail",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(exitCode).not.toBe(0);
// Windows flags require a Windows compile target
expect(stderr.toLowerCase()).toContain("windows compile target");
});
});
describe("Bun.build() API", () => {
test("all metadata via Bun.build()", async () => {
using dir = tempDir("windows-metadata-api", {
"app.js": `console.log("API metadata test");`,
});
const result = await Bun.build({
entrypoints: [join(String(dir), "app.js")],
outdir: String(dir),
compile: {
target: "bun-windows-x64",
outfile: "app-api.exe",
windows: {
title: "API App",
publisher: "API Company",
version: "3.0.0.0",
description: "Built with Bun.build API",
copyright: "© 2024 API Company",
},
},
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
const outfile = result.outputs[0].path;
await using _cleanup = cleanup(outfile);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("API App");
expect(getMetadata("CompanyName")).toBe("API Company");
expect(getMetadata("FileDescription")).toBe("Built with Bun.build API");
expect(getMetadata("LegalCopyright")).toBe("© 2024 API Company");
expect(getMetadata("ProductVersion")).toBe("3.0.0.0");
});
test("partial metadata via Bun.build()", async () => {
using dir = tempDir("windows-metadata-api-partial", {
"app.js": `console.log("Partial API test");`,
});
const result = await Bun.build({
entrypoints: [join(String(dir), "app.js")],
outdir: String(dir),
compile: {
target: "bun-windows-x64",
outfile: "partial-api.exe",
windows: {
title: "Partial App",
version: "1.0.0.0",
},
},
});
expect(result.success).toBe(true);
const outfile = result.outputs[0].path;
await using _cleanup = cleanup(outfile);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("Partial App");
expect(getMetadata("ProductVersion")).toBe("1.0.0.0");
});
test("relative outdir with compile", async () => {
using dir = tempDir("windows-relative-outdir", {
"app.js": `console.log("Relative outdir test");`,
});
const result = await Bun.build({
entrypoints: [join(String(dir), "app.js")],
outdir: "./out",
compile: {
target: "bun-windows-x64",
outfile: "relative.exe",
windows: {
title: "Relative Path App",
},
},
});
expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);
// Should not crash with assertion error
const exists = await Bun.file(result.outputs[0].path).exists();
expect(exists).toBe(true);
});
});
describe("Version string formats", () => {
const testVersionFormats = [
{ input: "1", expected: "1.0.0.0" },
{ input: "1.2", expected: "1.2.0.0" },
{ input: "1.2.3", expected: "1.2.3.0" },
{ input: "1.2.3.4", expected: "1.2.3.4" },
{ input: "10.20.30.40", expected: "10.20.30.40" },
{ input: "999.999.999.999", expected: "999.999.999.999" },
];
test.each(testVersionFormats)("version format: $input", async ({ input, expected }) => {
using dir = tempDir(`windows-version-${input.replace(/\./g, "-")}`, {
"app.js": `console.log("Version test");`,
});
const outfile = join(String(dir), "version-test.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-version",
input,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const version = execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.ProductVersion"`, {
encoding: "utf8",
}).trim();
expect(version).toBe(expected);
});
test("invalid version format should error gracefully", async () => {
using dir = tempDir("windows-invalid-version", {
"app.js": `console.log("Invalid version test");`,
});
const invalidVersions = [
"not.a.version",
"1.2.3.4.5",
"1.-2.3.4",
"65536.0.0.0", // > 65535
"",
];
for (const version of invalidVersions) {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
join(String(dir), "test.exe"),
"--windows-version",
version,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).not.toBe(0);
}
});
});
describe("Original Filename removal", () => {
test("Original Filename field should be empty", async () => {
using dir = tempDir("windows-original-filename", {
"app.js": `console.log("Original filename test");`,
});
const outfile = join(String(dir), "test-original.exe");
await using _cleanup = cleanup(outfile);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"Test Application",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
// Check that Original Filename is empty (not "bun.exe")
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
const originalFilename = getMetadata("OriginalFilename");
expect(originalFilename).toBe("");
expect(originalFilename).not.toBe("bun.exe");
});
test("Original Filename should be empty even with all metadata set", async () => {
using dir = tempDir("windows-original-filename-full", {
"app.js": `console.log("Full metadata test");`,
});
const outfile = join(String(dir), "full-metadata.exe");
await using _cleanup = cleanup(outfile);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"Complete App",
"--windows-publisher",
"Test Publisher",
"--windows-version",
"5.4.3.2",
"--windows-description",
"Application with full metadata",
"--windows-copyright",
"© 2024 Test",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
// Verify all custom metadata is set correctly
expect(getMetadata("ProductName")).toBe("Complete App");
expect(getMetadata("CompanyName")).toBe("Test Publisher");
expect(getMetadata("FileDescription")).toBe("Application with full metadata");
expect(getMetadata("ProductVersion")).toBe("5.4.3.2");
// But Original Filename should still be empty
const originalFilename = getMetadata("OriginalFilename");
expect(originalFilename).toBe("");
expect(originalFilename).not.toBe("bun.exe");
});
});
describe("Edge cases", () => {
test("long strings in metadata", async () => {
using dir = tempDir("windows-long-strings", {
"app.js": `console.log("Long strings test");`,
});
const longString = Buffer.alloc(255, "A").toString();
const outfile = join(String(dir), "long-strings.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
longString,
"--windows-description",
longString,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
});
test("special characters in metadata", async () => {
using dir = tempDir("windows-special-chars", {
"app.js": `console.log("Special chars test");`,
});
const outfile = join(String(dir), "special-chars.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"App™ with® Special© Characters",
"--windows-publisher",
"Company & Co.",
"--windows-description",
"Test \"quotes\" and 'apostrophes'",
"--windows-copyright",
"© 2024 <Company>",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toContain("App");
expect(getMetadata("CompanyName")).toContain("Company & Co.");
});
test("unicode in metadata", async () => {
using dir = tempDir("windows-unicode", {
"app.js": `console.log("Unicode test");`,
});
const outfile = join(String(dir), "unicode.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"アプリケーション",
"--windows-publisher",
"会社名",
"--windows-description",
"Émoji test 🚀 🎉",
"--windows-copyright",
"© 2024 世界",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
});
test("empty strings in metadata", async () => {
using dir = tempDir("windows-empty-strings", {
"app.js": `console.log("Empty strings test");`,
});
const outfile = join(String(dir), "empty.exe");
await using _cleanup = cleanup(outfile);
// Empty strings should be treated as not provided
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-title",
"",
"--windows-description",
"",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
});
});
describe("Combined with other compile options", () => {
test("metadata with --windows-hide-console", async () => {
using dir = tempDir("windows-metadata-hide-console", {
"app.js": `console.log("Hidden console test");`,
});
const outfile = join(String(dir), "hidden-with-metadata.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-hide-console",
"--windows-title",
"Hidden Console App",
"--windows-version",
"1.0.0.0",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("Hidden Console App");
expect(getMetadata("ProductVersion")).toBe("1.0.0.0");
});
test("metadata with --windows-icon", async () => {
// Create a simple .ico file (minimal valid ICO header)
const icoHeader = Buffer.from([
0x00,
0x00, // Reserved
0x01,
0x00, // Type (1 = ICO)
0x01,
0x00, // Count (1 image)
0x10, // Width (16)
0x10, // Height (16)
0x00, // Color count
0x00, // Reserved
0x01,
0x00, // Color planes
0x20,
0x00, // Bits per pixel
0x68,
0x01,
0x00,
0x00, // Size
0x16,
0x00,
0x00,
0x00, // Offset
]);
using dir = tempDir("windows-metadata-icon", {
"app.js": `console.log("Icon test");`,
"icon.ico": icoHeader,
});
const outfile = join(String(dir), "icon-with-metadata.exe");
await using proc = Bun.spawn({
cmd: [
bunExe(),
"build",
"--compile",
join(String(dir), "app.js"),
"--outfile",
outfile,
"--windows-icon",
join(String(dir), "icon.ico"),
"--windows-title",
"App with Icon",
"--windows-version",
"2.0.0.0",
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Icon might fail but metadata should still work
const exists = await Bun.file(outfile).exists();
expect(exists).toBe(true);
const getMetadata = (field: string) => {
try {
return execSync(`powershell -Command "(Get-ItemProperty '${outfile}').VersionInfo.${field}"`, {
encoding: "utf8",
}).trim();
} catch {
return "";
}
};
expect(getMetadata("ProductName")).toBe("App with Icon");
expect(getMetadata("ProductVersion")).toBe("2.0.0.0");
});
});
});
// Test for non-Windows platforms