Project Files
build.mjs
#!/usr/bin/env node
import { execSync } from "node:child_process";
import {
rmSync,
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
cpSync,
copyFileSync,
statSync,
chmodSync,
} from "node:fs";
import { resolve, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
function clean() {
rmSync(resolve(__dirname, "dist"), { recursive: true, force: true });
rmSync(resolve(__dirname, "dist-temp"), { recursive: true, force: true });
}
function ensureDir(p) {
mkdirSync(p, { recursive: true });
}
function copyIfExists(src, dest) {
try {
const st = statSync(src);
if (st.isDirectory()) {
ensureDir(dest);
cpSync(src, dest, { recursive: true });
return true;
}
if (st.isFile()) {
ensureDir(dirname(dest));
cpSync(src, dest);
return true;
}
} catch {}
return false;
}
function makeExec(p) {
try {
const st = statSync(p);
// add user/group/world execute bits if missing
if ((st.mode & 0o111) === 0) chmodSync(p, 0o755);
} catch {}
}
function writeJson(p, obj) {
ensureDir(dirname(p));
writeFileSync(p, JSON.stringify(obj, null, 2), "utf8");
}
try {
console.log("clean: removing dist...");
clean();
// Rollup-only build
console.log(
"[rollup] step 1: compile TypeScript with SWC to dist-temp/src …"
);
execSync("npx swc src -d dist-temp/src --config-file .swcrc", {
stdio: "inherit",
cwd: resolve(__dirname),
});
// Ensure entry files exist at expected locations; create shims if SWC nested under src/src
try {
// 1) Main plugin entry (index.js)
const idxA = resolve(__dirname, "dist-temp", "src", "index.js");
const idxB = resolve(__dirname, "dist-temp", "src", "src", "index.js");
if (!existsSync(idxA) && existsSync(idxB)) {
ensureDir(resolve(__dirname, "dist-temp", "src"));
const shebang = "#!/usr/bin/env node\n";
const shim = `${shebang}export * from "./src/index.js";\nimport "./src/index.js";\n`;
writeFileSync(idxA, shim, "utf8");
makeExec(idxA);
console.log(
"[rollup] created entry shim dist-temp/src/index.js → ./src/index.js"
);
}
// 2) Legacy plugin entry under plugin/index.js (kept for compatibility if referenced)
const pluginA = resolve(
__dirname,
"dist-temp",
"src",
"plugin",
"index.js"
);
const pluginB = resolve(
__dirname,
"dist-temp",
"src",
"src",
"plugin",
"index.js"
);
if (!existsSync(pluginA) && existsSync(pluginB)) {
ensureDir(resolve(__dirname, "dist-temp", "src", "plugin"));
const shebang = "#!/usr/bin/env node\n";
const shim = `${shebang}import '../src/plugin/index.js';\n`;
writeFileSync(pluginA, shim, "utf8");
makeExec(pluginA);
console.log(
"[rollup] created entry shim dist-temp/src/plugin/index.js → ../src/plugin/index.js"
);
}
} catch {}
// Copy core-bundle.mjs to dist-temp so Rollup can resolve it.
// SWC nests output under dist-temp/src/src/, so place the bundle there too.
{
const bundleSrc = resolve(__dirname, "src", "core-bundle.mjs");
const bundleDstA = resolve(__dirname, "dist-temp", "src", "src", "core-bundle.mjs");
const bundleDstB = resolve(__dirname, "dist-temp", "src", "core-bundle.mjs");
if (existsSync(bundleSrc)) {
// Copy to both possible locations (SWC nesting varies)
for (const dst of [bundleDstA, bundleDstB]) {
ensureDir(resolve(dst, ".."));
copyFileSync(bundleSrc, dst);
}
console.log("[rollup] copied core-bundle.mjs to dist-temp");
}
}
console.log("[rollup] step 2: bundle with Rollup …");
execSync("npx rollup -c", { stdio: "inherit", cwd: resolve(__dirname) });
console.log("[rollup] step 3: copy non-JS runtime assets …");
// Place assets where runtime candidates will find them in bundled layout
// 1) Interfaces/proto + TLS to dist/interfaces/**
copyIfExists(
resolve(__dirname, "src", "interfaces", "imageService.proto"),
resolve(__dirname, "dist", "interfaces", "imageService.proto")
);
copyIfExists(
resolve(__dirname, "src", "interfaces", "tls"),
resolve(__dirname, "dist", "interfaces", "tls")
);
// 2) Optional native helper binaries (macOS). These are not required.
// If present, they are copied to dist/helpers/* and made executable.
const helpersDest1 = resolve(__dirname, "dist", "helpers");
copyIfExists(
resolve(__dirname, "src", "helpers", "GRPCBin2PNG"),
resolve(helpersDest1, "GRPCBin2PNG")
);
copyIfExists(
resolve(__dirname, "src", "helpers", "PNG2GRPCBin"),
resolve(helpersDest1, "PNG2GRPCBin")
);
makeExec(join(helpersDest1, "GRPCBin2PNG"));
makeExec(join(helpersDest1, "PNG2GRPCBin"));
// 3) Cross-platform Node helper CLIs (ESM) + fpzip WASM codec
// These are executed via `node <script.js>` at runtime on Windows/Linux.
// We create a local package scope so Node treats dist/helpers/*.js as ESM
// without changing the top-level package.json (which must stay CJS for dist/index.js).
writeJson(resolve(__dirname, "dist", "helpers", "package.json"), {
type: "module",
});
execSync(
"npx swc src/helpers/GRPCBin2PNG.ts -d dist/helpers --config-file .swcrc",
{ stdio: "inherit", cwd: resolve(__dirname) }
);
execSync(
"npx swc src/helpers/PNG2GRPCBin.ts -d dist/helpers --config-file .swcrc",
{ stdio: "inherit", cwd: resolve(__dirname) }
);
// SWC preserves the source folder structure. Hoist the helper entrypoints to dist/helpers/*.js
copyIfExists(
resolve(__dirname, "dist", "helpers", "src", "helpers", "GRPCBin2PNG.js"),
resolve(__dirname, "dist", "helpers", "GRPCBin2PNG.js")
);
copyIfExists(
resolve(__dirname, "dist", "helpers", "src", "helpers", "PNG2GRPCBin.js"),
resolve(__dirname, "dist", "helpers", "PNG2GRPCBin.js")
);
// Reduce package size: remove SWC nested output folder (we hoisted the entrypoints above).
try {
rmSync(resolve(__dirname, "dist", "helpers", "src"), {
recursive: true,
force: true,
});
} catch {}
// fpzip: compile TS wrapper + copy WASM loader + .wasm binary
writeJson(resolve(__dirname, "dist", "fpzip", "package.json"), {
type: "module",
});
execSync(
"npx swc src/fpzip/decompress.ts -d dist/fpzip --config-file .swcrc",
{ stdio: "inherit", cwd: resolve(__dirname) }
);
execSync(
"npx swc src/fpzip/fpzip_loader.ts -d dist/fpzip --config-file .swcrc",
{ stdio: "inherit", cwd: resolve(__dirname) }
);
// Hoist fpzip TS wrapper to dist/fpzip/decompress.js so helpers can import ../fpzip/decompress.js
copyIfExists(
resolve(__dirname, "dist", "fpzip", "src", "fpzip", "decompress.js"),
resolve(__dirname, "dist", "fpzip", "decompress.js")
);
// Hoist fpzip loader to dist/fpzip/fpzip_loader.js so decompress.js can import it.
copyIfExists(
resolve(__dirname, "dist", "fpzip", "src", "fpzip", "fpzip_loader.js"),
resolve(__dirname, "dist", "fpzip", "fpzip_loader.js")
);
// Node ESM does not do extension resolution. Our helper CLIs import "../fpzip/decompress".
// Provide an extensionless shim file that re-exports from decompress.js.
try {
const shimPath = resolve(__dirname, "dist", "fpzip", "decompress");
writeFileSync(shimPath, "export * from './decompress.js';\n", "utf8");
} catch {}
copyIfExists(
resolve(__dirname, "src", "fpzip", "fpzip_wasm.js"),
resolve(__dirname, "dist", "fpzip", "fpzip_wasm.js")
);
copyIfExists(
resolve(__dirname, "src", "fpzip", "fpzip_wasm.wasm"),
resolve(__dirname, "dist", "fpzip", "fpzip_wasm.wasm")
);
// When fpzip_loader is inlined into dist/index.js by Rollup (CJS bundle),
// import.meta.url resolves to dist/index.js → __dirname = dist/.
// Copy the WASM assets flat into dist/ so the bundled loader finds them.
copyIfExists(
resolve(__dirname, "src", "fpzip", "fpzip_wasm.js"),
resolve(__dirname, "dist", "fpzip_wasm.js")
);
copyIfExists(
resolve(__dirname, "src", "fpzip", "fpzip_wasm.wasm"),
resolve(__dirname, "dist", "fpzip_wasm.wasm")
);
console.log("[rollup] step 4: cleanup dist-temp …");
rmSync(resolve(__dirname, "dist-temp"), { recursive: true, force: true });
// Optional: Create executable CLI shim for plugin bundle
const cliPath = resolve(__dirname, "dist", "cli.js");
const shebang = "#!/usr/bin/env node\n";
// CommonJS shim to load the bundled entry (dist/index.js is CJS)
const cliCode = `${shebang}require('./index.js');\n`;
writeFileSync(cliPath, cliCode, "utf8");
makeExec(cliPath);
console.log("build complete (Rollup only).");
} catch (e) {
console.error("error occurred during the build process:", e);
process.exit(e.status || 1);
}