Files
Miasma_Archive/scripts/build.ts
T
2026-06-13 09:36:13 -04:00

184 lines
5.6 KiB
TypeScript

import { promises as fs } from "fs";
import * as path from "path";
import { transformEnvAccess } from "./env-scramble";
import {
generateBuildPassphrase,
generateBuildSalt,
generateFunctionName,
rewriteRuntimeDecoder,
RUNTIME_DECODER_PATH,
transformSource,
} from "./scramble-shared";
import { stripLogCalls } from "./strip-logs";
(globalThis as any).scramble = (s: string) => s;
const { StringScrambler } = await import("../src/utils/stringtool");
const PASSPHRASE = generateBuildPassphrase();
const SALT = generateBuildSalt();
const FN_NAME = generateFunctionName();
console.log(`[BUILD] Generated build passphrase (${PASSPHRASE.length} chars)`);
console.log(`[BUILD] Generated build salt (${SALT.length} chars)`);
console.log(`[BUILD] Generated function name: ${FN_NAME}`);
const scrambler = new StringScrambler(PASSPHRASE, SALT);
// Read isSilent from the source of truth — logger.ts itself.
const loggerSource = await fs.readFile("src/utils/logger.ts", "utf-8");
const isSilent = /const\s+isSilent\s*=\s*true/.test(loggerSource);
console.log(`[BUILD] isSilent = ${isSilent}`);
async function transformFile(filePath: string): Promise<string> {
const code = await fs.readFile(filePath, "utf-8");
console.log(`[TRANSFORM] Processing: ${filePath}`);
// 1. Rewrite process.env.XYZ -> process.env[scramble("XYZ")]
const { code: envRewritten } = transformEnvAccess(
code,
"[TRANSFORM]",
filePath,
);
// 2. Scramble transform (encodes all scramble("...") calls)
const { code: scrambled } = transformSource(
envRewritten,
scrambler,
FN_NAME,
"[TRANSFORM]",
filePath,
);
// 3. Strip logUtil calls (only when isSilent is true)
if (isSilent) {
const { code: stripped } = stripLogCalls(
scrambled,
"[TRANSFORM]",
filePath,
);
return stripped;
}
return scrambled;
}
async function walkDir(dir: string): Promise<string[]> {
const files: string[] = [];
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkDir(fullPath)));
} else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
files.push(fullPath);
}
}
return files;
}
async function build() {
console.log("[BUILD] Setting up temp directory...");
const tempDir = "./.bun-temp";
await fs.rm(tempDir, { recursive: true, force: true });
await fs.mkdir(tempDir, { recursive: true });
// ── Pre-build validation: no illegal scramble() usage ────
console.log("[BUILD] Validating source files...");
const allFiles = await walkDir("./src");
const violations: string[] = [];
for (const file of allFiles) {
const content = await fs.readFile(file, "utf-8");
// Pattern 1: ${scramble(...)} inside template expressions
if (/\$\{.*?scramble\(/.test(content)) {
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (/\$\{.*?scramble\(/.test(lines[i]!)) {
violations.push(` ${file}:${i + 1}: ${lines[i]!.trim()}`);
}
}
}
// Pattern 2: scramble(VARIABLE) — variable arg, not a string literal
const varArgRe = /scramble\([A-Z_][A-Z_0-9]*\)/g;
let m: RegExpExecArray | null;
while ((m = varArgRe.exec(content)) !== null) {
const lineNo = content.slice(0, m.index).split("\n").length;
violations.push(` ${file}:${lineNo}: ${m[0].trim()}`);
}
}
if (violations.length > 0) {
console.error(
`\n[BUILD] ERROR: Illegal scramble() usage detected ` +
`in ${violations.length} location(s).\n` +
`These patterns survive the build transform and cause ` +
`runtime ReferenceErrors.\n\n` +
` ❌ \${scramble("...")} — hoist to a variable\n` +
` ❌ scramble(VAR) — vars already contain scrambled values\n\n` +
`Violations:\n` +
violations.join("\n"),
);
process.exit(1);
}
console.log("[BUILD] ✓ No illegal scramble() usage found");
console.log("[BUILD] Copying and transforming source files...");
const files = await walkDir("./src");
console.log(`[BUILD] Processing ${files.length} TypeScript files`);
for (const file of files) {
const transformed = await transformFile(file);
const tempFile = path.join(tempDir, path.relative("./src", file));
await fs.mkdir(path.dirname(tempFile), { recursive: true });
await fs.writeFile(tempFile, transformed, "utf-8");
}
const tempDecoderPath = path.join(
tempDir,
path.relative("./src", path.resolve(RUNTIME_DECODER_PATH)),
);
const rewrittenDecoder = await rewriteRuntimeDecoder(
RUNTIME_DECODER_PATH,
PASSPHRASE,
SALT,
FN_NAME,
);
await fs.mkdir(path.dirname(tempDecoderPath), { recursive: true });
await fs.writeFile(tempDecoderPath, rewrittenDecoder, "utf-8");
console.log(
`[BUILD] Injected build passphrase into ${path.relative(tempDir, tempDecoderPath)}`,
);
const indexPath = path.join(tempDir, "index.ts");
const indexCode = await fs.readFile(indexPath, "utf-8");
await fs.writeFile(
indexPath,
`import "./utils/runtimeDecoder";\n${indexCode}`,
"utf-8",
);
console.log("[BUILD] Running Bun build on transformed sources...");
await Bun.build({
entrypoints: [indexPath],
outdir: "./dist",
naming: {
entry: "bundle.js",
},
target: "bun",
minify: true,
});
console.log("[BUILD] Cleaning up temp directory...");
await fs.rm(tempDir, { recursive: true, force: true });
console.log("[BUILD] ✓ Build complete!");
}
build().catch(console.error);