164 lines
5.3 KiB
JavaScript
164 lines
5.3 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import uglify from 'uglify-js';
|
|
import { Buffer } from 'buffer';
|
|
|
|
const version = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;
|
|
const cfg = JSON.parse(fs.readFileSync(process.argv[2] || './bin/bundle.config.json', 'utf8'));
|
|
const { inputs, outputs, excludes, compress } = cfg;
|
|
const excludePre = (excludes ?? []).filter(s => s.indexOf('*') >= 0).map(s => s.replaceAll('*',''));
|
|
const envcomp = process.env.COMPRESS ? JSON.parse(process.env.COMPRESS) : undefined;
|
|
const debug = process.env.DEBUG;
|
|
|
|
// --- recursive directory walker (follows symlinks safely) ---
|
|
function* walk(dir, seen = new Set()) {
|
|
dir = path.normalize(dir);
|
|
if (excludes.indexOf(dir) >= 0) {
|
|
return;
|
|
}
|
|
if (!fs.existsSync(dir)) {
|
|
return;
|
|
}
|
|
|
|
const real = fs.realpathSync(dir);
|
|
if (seen.has(real)) {
|
|
return;
|
|
}
|
|
seen.add(real);
|
|
|
|
for (const name of fs.readdirSync(dir)) {
|
|
const p = path.join(dir, name);
|
|
let st;
|
|
try { st = fs.statSync(p); } catch { continue; }
|
|
if (st.isDirectory()) yield* walk(p, seen);
|
|
else yield p;
|
|
}
|
|
}
|
|
|
|
// --- collect all files according to config ---
|
|
let entries = [];
|
|
let virtMap = new Set();
|
|
for (const input of inputs) {
|
|
const { root, prefix, src, dst } = input;
|
|
|
|
if (src && dst) {
|
|
if (virtMap.has(dst)) {
|
|
console.log('pre-existing', dst);
|
|
} else {
|
|
entries.push({ file: src, virt: dst });
|
|
virtMap.add(dst);
|
|
}
|
|
}
|
|
|
|
if (root)
|
|
for (const file of walk(root)) {
|
|
const rel = path.relative(root, file).replace(/\\/g, '/');
|
|
const virt = prefix ? `${prefix}/${rel}` : rel;
|
|
if (virtMap.has(virt)) {
|
|
console.log('pre-existing', virt, rel, root);
|
|
} else {
|
|
entries.push({ file, virt });
|
|
virtMap.add(virt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- build header and data ---
|
|
const dataParts = [];
|
|
const entryMeta = [];
|
|
let offset = 0;
|
|
let cache = new Map();
|
|
let compEnable = (envcomp ?? compress);
|
|
let cacheDir = cfg.cacheDir ?? ".bcache";
|
|
|
|
if (compEnable) {
|
|
fs.mkdirSync(cacheDir, { recursive: true });
|
|
}
|
|
|
|
function fileTime(path) {
|
|
try {
|
|
return (fs.statSync(path).ctimeMs / 1000) | 0;
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
for (const { file, virt } of entries) {
|
|
if (excludes.indexOf(file) >= 0) {
|
|
continue;
|
|
}
|
|
if (excludePre.filter(ex => file.startsWith(ex)).length) {
|
|
continue;
|
|
}
|
|
let record = cache.get(file);
|
|
if (record) {
|
|
if (debug) console.log({ alias: virt, from: file });
|
|
entryMeta.push({ ...record, virt });
|
|
} else {
|
|
if (debug) console.log({ write: virt, from: file });
|
|
let data = fs.readFileSync(file);
|
|
if (compEnable && file.endsWith("js")) {
|
|
let cpath = path.resolve(cacheDir, file);
|
|
if (fileTime(cpath) > fileTime(file)) {
|
|
data = fs.readFileSync(cpath);
|
|
// console.log('cached', file);
|
|
} else {
|
|
console.log('compress', file);
|
|
data = uglify.minify(data.toString(), {
|
|
compress: {
|
|
merge_vars: false,
|
|
unused: false
|
|
}
|
|
});
|
|
// console.log({ to: typeof(data), data });
|
|
if (!data.code) {
|
|
if (debug) console.log({ skip: file });
|
|
continue;
|
|
}
|
|
data = Buffer.from(data.code);
|
|
fs.mkdirSync(path.dirname(cpath), { recursive: true });
|
|
fs.writeFileSync(cpath, data);
|
|
}
|
|
}
|
|
record = { virt, offset, length: data.length };
|
|
cache.set(file, record);
|
|
entryMeta.push(record);
|
|
dataParts.push(data);
|
|
offset += data.length;
|
|
if (debug) console.log('write', virt, data.length);
|
|
}
|
|
}
|
|
|
|
// temporary header without offsets to measure total size
|
|
let tempHeaderParts = [];
|
|
for (const { virt, offset, length } of entryMeta) {
|
|
const name = Buffer.from(virt, 'utf8');
|
|
const nlen = Buffer.alloc(2); nlen.writeUInt16LE(name.length);
|
|
const offb = Buffer.alloc(4); offb.writeUInt32LE(0); // placeholder
|
|
const lenb = Buffer.alloc(4); lenb.writeUInt32LE(length);
|
|
tempHeaderParts.push(nlen, name, offb, lenb);
|
|
}
|
|
const count = Buffer.alloc(4); count.writeUInt32LE(entries.length);
|
|
const tempHeader = Buffer.concat([count, ...tempHeaderParts]);
|
|
const headerSize = tempHeader.length; // true data offset base
|
|
|
|
// now rebuild header with correct absolute offsets
|
|
let headerParts = [];
|
|
for (const { virt, offset, length } of entryMeta) {
|
|
const name = Buffer.from(virt, 'utf8');
|
|
const nlen = Buffer.alloc(2); nlen.writeUInt16LE(name.length);
|
|
const offb = Buffer.alloc(4); offb.writeUInt32LE(headerSize + offset);
|
|
const lenb = Buffer.alloc(4); lenb.writeUInt32LE(length);
|
|
headerParts.push(nlen, name, offb, lenb);
|
|
// console.log(virt);
|
|
}
|
|
|
|
const header = Buffer.concat([count, ...headerParts]);
|
|
const full = Buffer.concat([header, ...dataParts]);
|
|
|
|
// --- write final bundle ---
|
|
const outPath = (outputs.bundle || './bundle.bin').replace('{version}', version);
|
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
fs.writeFileSync(outPath, full); // ⚡ write raw binary
|
|
|
|
console.log(`✅ Bundled ${entries.length} files → ${outPath} (${full.length} bytes)`);
|