const fs = require("node:fs");
const path = require("node:path");
const crypto = require("node:crypto");
const { spawnSync } = require("node:child_process");
const ROOT = path.resolve(__dirname, "..");
const OUTPUT_DIR = path.join(ROOT, "frontend_package");
const OUTPUT_ZIP = path.join(OUTPUT_DIR, "lotus-frontend.zip");
const OUTPUT_MANIFEST = path.join(OUTPUT_DIR, "frontend-manifest.json");
const SOURCE_MODE = (process.env.LOTUS_SOURCE || "auto").toLowerCase();
const LOCAL_PATH = path.resolve(ROOT, process.env.LOTUS_LOCAL_PATH || "../lotus");
const PACKAGE_NAME = process.env.LOTUS_PACKAGE_NAME || "@bigduu/lotus";
const PREBUILT_DIST_DIR = process.env.LOTUS_DIST_DIR
? path.resolve(ROOT, process.env.LOTUS_DIST_DIR)
: path.resolve(ROOT, "../lotus/dist");
function fail(message) {
console.error(`❌ ${message}`);
process.exit(1);
}
function fileExists(target) {
try {
return fs.statSync(target).isFile();
} catch {
return false;
}
}
function dirExists(target) {
try {
return fs.statSync(target).isDirectory();
} catch {
return false;
}
}
function localLotusExists() {
return dirExists(LOCAL_PATH) && fs.existsSync(path.join(LOCAL_PATH, "package.json"));
}
function resolvePackageRoot() {
try {
const pkgJsonPath = require.resolve(`${PACKAGE_NAME}/package.json`, {
paths: [ROOT],
});
return path.dirname(pkgJsonPath);
} catch {
return null;
}
}
function runNpmScript(prefix, script) {
const isWindows = process.platform === "win32";
let result = spawnSync("npm", ["run", script], {
stdio: "inherit",
env: process.env,
cwd: prefix,
shell: isWindows,
});
if (typeof result.status === "number") {
if (result.status !== 0) process.exit(result.status);
return;
}
if (isWindows) {
const fallbackCommand = `npm run ${script}`;
result = spawnSync(fallbackCommand, {
stdio: "inherit",
env: process.env,
cwd: prefix,
shell: true,
});
if (typeof result.status === "number") {
if (result.status !== 0) process.exit(result.status);
return;
}
}
fail(`Failed to run npm script "${script}" in ${prefix}`);
}
function hasExistingStagedPackage() {
return fileExists(OUTPUT_ZIP) && fileExists(OUTPUT_MANIFEST);
}
function reuseExistingStagedPackage(reason) {
if (!hasExistingStagedPackage()) {
return false;
}
console.log(`ℹ️ ${reason}`);
console.log(`ℹ️ Reusing committed frontend package: ${OUTPUT_ZIP}`);
console.log(`ℹ️ Reusing committed frontend manifest: ${OUTPUT_MANIFEST}`);
return true;
}
function resolveSource() {
if (!["auto", "local", "package"].includes(SOURCE_MODE)) {
fail(`Invalid LOTUS_SOURCE="${SOURCE_MODE}" (expected auto|local|package)`);
}
const localAvailable = localLotusExists();
const packageRoot = resolvePackageRoot();
if (SOURCE_MODE === "local") {
if (!localAvailable) {
fail(
`LOTUS_SOURCE=local but local Lotus not found at ${LOCAL_PATH}. Set LOTUS_LOCAL_PATH or use LOTUS_SOURCE=package.`,
);
}
return { mode: "local", packageRoot: null };
}
if (SOURCE_MODE === "package") {
if (!packageRoot) {
fail(
`LOTUS_SOURCE=package but package "${PACKAGE_NAME}" is not installed. Install it or use LOTUS_SOURCE=local.`,
);
}
return { mode: "package", packageRoot };
}
if (localAvailable) {
return { mode: "local", packageRoot: null };
}
if (packageRoot) {
return { mode: "package", packageRoot };
}
return null;
}
function listFilesRecursively(rootDir) {
const files = [];
const walk = (currentDir) => {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const absolutePath = path.join(currentDir, entry.name);
const relativePath = path.relative(rootDir, absolutePath).replace(/\\/g, "/");
if (entry.isDirectory()) {
walk(absolutePath);
} else if (entry.isFile()) {
files.push({ absolutePath, relativePath });
}
}
};
walk(rootDir);
return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
}
function computeDirectoryHash(rootDir) {
const hash = crypto.createHash("sha256");
for (const file of listFilesRecursively(rootDir)) {
hash.update(file.relativePath);
hash.update("\0");
hash.update(fs.readFileSync(file.absolutePath));
hash.update("\0");
}
return `sha256:${hash.digest("hex")}`;
}
function resolveLotusVersion(source) {
const pkgPath =
source.mode === "local"
? path.join(LOCAL_PATH, "package.json")
: path.join(source.packageRoot, "package.json");
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
}
function resolveDistDir(source) {
if (source.mode === "local") {
console.log(`ℹ️ Using local Lotus at ${LOCAL_PATH}`);
runNpmScript(LOCAL_PATH, "build");
return path.join(LOCAL_PATH, "dist");
}
console.log(`ℹ️ Using packaged Lotus "${PACKAGE_NAME}" at ${source.packageRoot}`);
return path.join(source.packageRoot, "dist");
}
function resolvePrebuiltDistDir() {
if (!dirExists(PREBUILT_DIST_DIR)) {
fail(`Prebuilt Lotus dist directory not found: ${PREBUILT_DIST_DIR}`);
}
return PREBUILT_DIST_DIR;
}
function resolvePrebuiltLotusVersion() {
const pkgPath = path.join(LOCAL_PATH, "package.json");
if (!fs.existsSync(pkgPath)) {
fail(`Cannot resolve Lotus version for prebuilt dist; missing package.json at ${pkgPath}`);
}
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
}
function resetOutputDir() {
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
function writeManifest(stageDir, version) {
const manifest = {
schema_version: 1,
frontend_name: "lotus",
frontend_version: version,
bundle_hash: computeDirectoryHash(stageDir),
built_at: new Date().toISOString(),
entry: "index.html",
};
const content = `${JSON.stringify(manifest, null, 2)}\n`;
fs.writeFileSync(path.join(stageDir, "frontend-manifest.json"), content);
fs.writeFileSync(OUTPUT_MANIFEST, content);
return manifest;
}
function createZipFromStage(stageDir) {
const isWindows = process.platform === "win32";
const zipName = path.basename(OUTPUT_ZIP);
const args = isWindows ? ["a", "-tzip", zipName, "."] : ["-r", zipName, "."];
const result = isWindows
? spawnSync("7z", args, { cwd: stageDir, stdio: "inherit", env: process.env, shell: true })
: spawnSync("zip", args, { cwd: stageDir, stdio: "inherit", env: process.env });
if (result.status !== 0) {
fail(`Failed to create frontend zip package from ${stageDir}`);
}
fs.renameSync(path.join(stageDir, zipName), OUTPUT_ZIP);
}
function stagePackageFromDist(distDir, version) {
if (!dirExists(distDir)) {
fail(`Lotus dist directory not found: ${distDir}`);
}
resetOutputDir();
const stageDir = path.join(OUTPUT_DIR, ".stage");
fs.mkdirSync(stageDir, { recursive: true });
fs.cpSync(distDir, stageDir, { recursive: true });
const manifest = writeManifest(stageDir, version);
createZipFromStage(stageDir);
fs.rmSync(stageDir, { recursive: true, force: true });
console.log(`✅ Created embedded frontend package: ${OUTPUT_ZIP}`);
console.log(`✅ Wrote embedded frontend manifest: ${OUTPUT_MANIFEST}`);
console.log(`ℹ️ Embedded frontend version: ${manifest.frontend_version}`);
console.log(`ℹ️ Embedded frontend hash: ${manifest.bundle_hash}`);
}
function stagePackage() {
const source = resolveSource();
if (!source) {
if (
reuseExistingStagedPackage(
`No Lotus source found at ${LOCAL_PATH} and package "${PACKAGE_NAME}" is not installed.`,
)
) {
return;
}
fail(
`No Lotus source found. Expected local checkout at ${LOCAL_PATH} or installed package "${PACKAGE_NAME}".`,
);
}
stagePackageFromDist(resolveDistDir(source), resolveLotusVersion(source));
}
function stagePrebuiltPackage() {
stagePackageFromDist(resolvePrebuiltDistDir(), resolvePrebuiltLotusVersion());
}
function printInfo() {
const localAvailable = localLotusExists();
const packageRoot = resolvePackageRoot();
console.log(`LOTUS_SOURCE=${SOURCE_MODE}`);
console.log(`LOTUS_LOCAL_PATH=${LOCAL_PATH} (${localAvailable ? "found" : "missing"})`);
console.log(
`LOTUS_PACKAGE_NAME=${PACKAGE_NAME} (${packageRoot ? `found at ${packageRoot}` : "missing"})`,
);
}
const command = process.argv[2] || "stage";
if (command === "stage") {
stagePackage();
process.exit(0);
}
if (command === "stage:prebuilt") {
stagePrebuiltPackage();
process.exit(0);
}
if (command === "info") {
printInfo();
process.exit(0);
}
fail(`Unknown command "${command}". Use one of: stage, stage:prebuilt, info.`);