179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
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`);
|