kusamoji
Version:
Japanese morphological analyzer for Node.js — Viterbi tokenizer with mmap dict loading and pluggable POS-source strategy
171 lines (155 loc) • 5.49 kB
JavaScript
;
/**
* Native mmap addon loader.
*
* Resolution order:
* 1. ~/.kusamoji/mmap_addon-{platform}-{arch}-napi{ver}.node (user cache)
* 2. prebuilds/{platform}-{arch}/mmap_addon.node (shipped pre-built)
* 3. build/Release/mmap_addon.node (locally compiled via node-gyp)
* 4. null (fallback — NodeDictionaryLoader uses fs.readFile)
*
* The cache at ~/.kusamoji/ survives npm upgrades and re-installs.
* A config.json alongside the binary tracks Node/N-API versions so
* stale binaries are detected and skipped.
*/
let path = require("path");
let fs = require("fs");
let os = require("os");
const PLATFORM = process.platform;
const ARCH = process.arch;
const NAPI_VER = process.versions.napi;
const BINARY_NAME = "mmap_addon.node";
const CACHE_BINARY = `mmap_addon-${PLATFORM}-${ARCH}-napi${NAPI_VER}.node`;
const CACHE_DIR = path.join(os.homedir(), ".kusamoji");
const NATIVE_DIR = path.resolve(__dirname);
/**
* Try to load a .node binary. Returns the exports or null.
*/
function tryLoad(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
let addon = require(filePath);
if (typeof addon.mmapFile === "function") return addon;
} catch (e) { /* binary exists but can't load — wrong ABI, corrupt, etc. */ }
return null;
}
/**
* Read the cache config, if it exists.
*/
function readCacheConfig() {
try {
let raw = fs.readFileSync(path.join(CACHE_DIR, "config.json"), "utf8");
return JSON.parse(raw);
} catch { return null; }
}
/**
* Write cache config alongside the cached binary.
*/
function writeCacheConfig(source) {
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.writeFileSync(path.join(CACHE_DIR, "config.json"), JSON.stringify({
builtAt: new Date().toISOString(),
nodeVersion: process.version,
napiVersion: NAPI_VER,
platform: PLATFORM,
arch: ARCH,
source: source,
}, null, 2) + "\n");
} catch { /* non-critical */ }
}
/**
* Load the mmap addon binary. Returns { mmapFile } or null.
*
* @returns {object|null}
*/
function loadMmapAddon() {
// 1. User cache (~/.kusamoji/)
let cachePath = path.join(CACHE_DIR, CACHE_BINARY);
let config = readCacheConfig();
if (config && config.napiVersion === NAPI_VER && config.platform === PLATFORM && config.arch === ARCH) {
let addon = tryLoad(cachePath);
if (addon) return addon;
}
// 2. Shipped prebuilt
let prebuiltPath = path.join(NATIVE_DIR, "prebuilds", `${PLATFORM}-${ARCH}`, BINARY_NAME);
let addon = tryLoad(prebuiltPath);
if (addon) {
// Cache it for future use (survives npm reinstall)
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.copyFileSync(prebuiltPath, cachePath);
writeCacheConfig("prebuilt");
} catch { /* non-critical */ }
return addon;
}
// 3. Locally compiled (node-gyp rebuild)
let localPath = path.join(NATIVE_DIR, "build", "Release", BINARY_NAME);
addon = tryLoad(localPath);
if (addon) {
// Cache it
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.copyFileSync(localPath, cachePath);
writeCacheConfig("compiled");
} catch { /* non-critical */ }
return addon;
}
// 4. No binary available — mmap not used
return null;
}
/**
* Install hook — called by postinstall script.
* Tries prebuilt → compile → silent skip.
*/
function install() {
let addon = loadMmapAddon();
if (addon) {
console.log(`[kusamoji] mmap addon ready (${PLATFORM}-${ARCH}, N-API ${NAPI_VER})`);
return true;
}
// Try to compile from source
try {
let { execSync } = require("child_process");
console.log("[kusamoji] no prebuilt binary found, compiling from source...");
execSync("npx node-gyp rebuild", {
cwd: NATIVE_DIR,
stdio: "inherit",
timeout: 120000,
});
addon = tryLoad(path.join(NATIVE_DIR, "build", "Release", BINARY_NAME));
if (addon) {
try {
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.copyFileSync(
path.join(NATIVE_DIR, "build", "Release", BINARY_NAME),
path.join(CACHE_DIR, CACHE_BINARY)
);
writeCacheConfig("compiled");
} catch { }
console.log(`[kusamoji] mmap addon compiled and cached at ~/.kusamoji/`);
return true;
}
} catch (e) {
// Compile failed — not fatal
}
console.log("[kusamoji] mmap addon not available (optional — dict loading uses fs.readFile fallback)");
return false;
}
// CLI mode: node loader.js --install
if (require.main === module) {
let arg = process.argv[2];
if (arg === "--install") {
install();
} else if (arg === "--info") {
let config = readCacheConfig();
console.log("Platform:", PLATFORM + "-" + ARCH);
console.log("N-API:", NAPI_VER);
console.log("Cache dir:", CACHE_DIR);
console.log("Cache config:", config || "(none)");
console.log("Addon:", loadMmapAddon() ? "LOADED" : "NOT AVAILABLE");
} else {
console.log("Usage: node loader.js --install | --info");
}
}
module.exports = { loadMmapAddon, install, CACHE_DIR, NATIVE_DIR };