Files

179 lines
5.3 KiB
TypeScript
Raw Permalink Normal View History

2026-08-27 21:09:14 +00:00
import * as fs from "fs";
import path from "path";
interface LetterGroup {
offset: number;
length: number;
packages: string[];
}
const INPUT = path.join(__dirname, "..", "src", "runtime", "cli", "add_completions.txt");
const OUTPUT = path.join(__dirname, "..", "src", "runtime", "cli", "add_completions.rs");
// Read and parse input file
const content = fs.readFileSync(INPUT, "utf8");
const packages = content
.split("\n")
.map(line => line.trim())
.filter(line => line.length > 0)
.sort();
// Group packages by first letter
const letterGroups = new Map<string, LetterGroup>();
let currentOffset = 0;
let maxListSize = 0;
for (const pkg of packages) {
if (pkg.length === 0) continue;
const firstLetter = pkg[0].toLowerCase();
if (!letterGroups.has(firstLetter)) {
letterGroups.set(firstLetter, {
offset: currentOffset,
length: 0,
packages: [],
});
}
const group = letterGroups.get(firstLetter)!;
group.packages.push(pkg);
group.length++;
maxListSize = Math.max(maxListSize, group.length);
}
// Create a single buffer with all package data
const dataChunks: Buffer[] = [];
let totalUncompressed = 0;
// Store total package count first
const totalCountBuf = Buffer.alloc(4);
totalCountBuf.writeUInt32LE(packages.length, 0);
dataChunks.push(totalCountBuf);
totalUncompressed += 4;
// Then all packages with length prefixes
for (const pkg of packages) {
const lenBuf = Buffer.alloc(2);
lenBuf.writeUInt16LE(pkg.length, 0);
dataChunks.push(lenBuf);
dataChunks.push(Buffer.from(pkg, "utf8"));
totalUncompressed += 2 + pkg.length;
}
const uncompressedData = Buffer.concat(dataChunks);
// Compress with zstd (level 1, matching the old `zstd -1` CLI invocation)
const compressedData = Bun.zstdCompressSync(uncompressedData, { level: 1 });
// Calculate compression ratio
const totalCompressed = compressedData.length;
const ratio = ((totalCompressed / totalUncompressed) * 100).toFixed(1);
console.log("\nCompression statistics:");
console.log(`Uncompressed size: ${totalUncompressed} bytes`);
console.log(`Compressed size: ${totalCompressed} bytes`);
console.log(`Compression ratio: ${ratio}%`);
// Generate index entries
const letters = "abcdefghijklmnopqrstuvwxyz";
const indexEntries: string[] = [];
let offset = 0;
for (const letter of letters) {
const group = letterGroups.get(letter);
const len = group?.length ?? 0;
indexEntries.push(` (${offset}, ${len}), // ${letter}`);
offset += len;
}
// Split the compressed blob into 16-byte rows for readability
const blobLines: string[] = [];
for (let i = 0; i < compressedData.length; i += 16) {
const row = [...compressedData.slice(i, i + 16)].join(", ");
blobLines.push(` ${row},`);
}
const out = `// Auto-generated by misctools/generate-add-completions.ts. Do not edit.
//
// Regenerate after editing src/runtime/cli/add_completions.txt:
//
// bun misctools/generate-add-completions.ts
//
// Compressing the completions list saves about 100 KB of binary size.
use bun_core::Once;
#[repr(u8)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub(crate) enum FirstLetter {
${[...letters].map(l => ` ${l.toUpperCase()} = b'${l}',`).join("\n")}
}
/// Largest per-letter package list length.
pub(crate) const BIGGEST_LIST: usize = ${maxListSize};
const UNCOMPRESSED_SIZE: usize = ${totalUncompressed};
/// (offset, length) into the decompressed package list for each letter a-z.
const INDEX: [(usize, usize); 26] = [
${indexEntries.join("\n")}
];
static COMPRESSED_DATA: &[u8] = &[
${blobLines.join("\n")}
];
struct Table {
_decompressed: Vec<u8>,
packages: Vec<&'static [u8]>,
}
static TABLE: Once<Table> = <Once<Table>>::new();
fn build_table() -> Table {
let decompressed = bun_zstd::decompress_alloc(COMPRESSED_DATA)
.expect("add_completions: zstd decompress failed");
debug_assert_eq!(decompressed.len(), UNCOMPRESSED_SIZE);
// SAFETY: the decompressed Vec is moved into TABLE (a process-lifetime
// Once) and never dropped or reallocated, so its storage is valid for
// 'static. The package slices below borrow this storage.
let data: &'static [u8] =
unsafe { core::slice::from_raw_parts(decompressed.as_ptr(), decompressed.len()) };
let total = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut packages: Vec<&'static [u8]> = Vec::with_capacity(total);
let mut pos: usize = 4;
for _ in 0..total {
let len = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
packages.push(&data[pos..pos + len]);
pos += len;
}
Table { _decompressed: decompressed, packages }
}
/// Decompress the package-name table. Idempotent.
pub fn init() {
TABLE.get_or_init(build_table);
}
/// Returns the slice of package names beginning with \`letter\`.
pub(crate) fn get_packages(letter: FirstLetter) -> &'static [&'static [u8]] {
let table = TABLE.get_or_init(build_table);
let (offset, length) = INDEX[(letter as u8 - b'a') as usize];
if length == 0 {
return &[];
}
&table.packages[offset..offset + length]
}
`;
fs.writeFileSync(OUTPUT, out);
try {
const { execSync } = await import("child_process");
execSync(`rustfmt "${OUTPUT}"`, { stdio: "inherit" });
} catch {
// rustfmt not available; committed output is already formatted reasonably
}
console.log(`\nGenerated ${OUTPUT} for ${packages.length} packages`);