vite-plugin-native
Version:
Supports Node/Electron C/C++ native addons
347 lines (346 loc) • 11.8 kB
JavaScript
import fs from "node:fs";
import path from "node:path";
import { normalizePath as normalizePath$1 } from "vite";
import os from "node:os";
import { flatDependencies } from "dependencies-tree";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import glob from "fast-glob";
import _libEsm from "lib-esm";
const isWindows = os.platform() === "win32";
function slash(p) {
return p.replace(/\\/g, "/");
}
function normalizePath(id) {
return path.posix.normalize(isWindows ? slash(id) : id);
}
const COLOURS = {
$: (c) => (str) => `\x1B[${c}m` + str + "\x1B[0m",
gary: (str) => COLOURS.$(90)(str),
cyan: (str) => COLOURS.$(36)(str),
yellow: (str) => COLOURS.$(33)(str),
green: (str) => COLOURS.$(32)(str),
red: (str) => COLOURS.$(31)(str)
};
const VOLUME_RE = /^[A-Z]:/i;
function node_modules(root, paths = []) {
if (!root)
return paths;
if (!(root.startsWith("/") || VOLUME_RE.test(root)))
return paths;
const p = path.posix.join(normalizePath(root), "node_modules");
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
paths = paths.concat(p);
}
root = path.posix.join(root, "..");
return root === "/" || /^[A-Z]:$/i.test(root) ? paths : node_modules(root, paths);
}
const libEsm = _libEsm.default || _libEsm;
const cjs$1 = createCjs(import.meta.url);
function createCjs(url = import.meta.url) {
const cjs__filename = typeof __filename === "undefined" ? fileURLToPath(url) : __filename;
const cjs__dirname = path.dirname(cjs__filename);
const cjsRequire = typeof require === "undefined" ? createRequire(url) : require;
return {
__filename: cjs__filename,
__dirname: cjs__dirname,
require: cjsRequire
};
}
async function globNativeFiles(cwd) {
const nativeFiles = await glob("**/*.node", { cwd });
return nativeFiles;
}
async function getDependenciesNatives(root = process.cwd()) {
const node_modules_paths = node_modules(root);
const natives = /* @__PURE__ */ new Map();
for (const node_modules_path of node_modules_paths) {
const pkgId = path.join(node_modules_path, "../package.json");
if (fs.existsSync(pkgId)) {
const pkg = cjs$1.require(pkgId);
const deps = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.devDependencies || {}));
for (const dep of deps) {
if (natives.has(dep))
continue;
const depPath = path.posix.join(node_modules_path, dep);
const nativeFiles = await globNativeFiles(depPath);
if (nativeFiles.length) {
natives.set(dep, {
name: dep,
type: "dependencies",
path: depPath,
nativeFiles
});
}
}
}
}
return natives;
}
function getInteropSnippet(name, id) {
const snippet = libEsm({
exports: Object.getOwnPropertyNames(cjs$1.require(name))
});
return `
import { createRequire } from "module";
const cjsRequire = createRequire(import.meta.url);
const _M_ = cjsRequire("${id}");
${snippet.exports}
`;
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
return dir;
}
async function resolveNativeRecord(source, importer) {
let modulePath;
try {
const modulePackageJson = cjs$1.require.resolve(`${source}/package.json`, {
paths: [importer]
});
modulePath = path.dirname(modulePackageJson);
} catch {
}
if (modulePath) {
const nativeFiles = await globNativeFiles(modulePath);
if (nativeFiles.length) {
return (/* @__PURE__ */ new Map()).set(source, {
name: source,
type: "detected",
path: modulePath,
nativeFiles
});
}
}
}
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file);
const destFile = path.resolve(destDir, file);
copy(srcFile, destFile);
}
}
function copy(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
copyDir(src, dest);
} else {
fs.copyFileSync(src, dest);
}
}
const cjs = createCjs(import.meta.url);
const TAG = "[vite-plugin-native]";
const loader1 = "@vercel/webpack-asset-relocator-loader";
const outputAssetBase = "native_modules";
const NativeExt = ".native.cjs";
const InteropExt = ".interop.mjs";
const nativesMap = /* @__PURE__ */ new Map();
const scopedPackagePattern = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/;
function native(options) {
const assetsDir = options.assetsDir ?? (options.assetsDir = "node_natives");
let output;
return {
name: "vite-plugin-native",
async config(config) {
var _a;
const resolvedRoot = normalizePath$1(config.root ? path.resolve(config.root) : process.cwd());
const outDir = ((_a = config.build) == null ? void 0 : _a.outDir) ?? "dist";
output = normalizePath$1(path.join(resolvedRoot, outDir, assetsDir));
let nativeRecord = await getDependenciesNatives(resolvedRoot);
if (options.natives) {
Array.isArray(options.natives) ? options.natives : options.natives([...nativeRecord.keys()]);
}
const withDistAssetBase = (p) => assetsDir && p ? `${assetsDir}/${p}` : p;
const alias = {
find: /^(?!(?:\/?@vite\/|\.))(.*)/,
// Keep `customResolver` receive original source.
// @see https://github.com/rollup/plugins/blob/alias-v5.1.0/packages/alias/src/index.ts#L92
replacement: "$1",
async customResolver(source, importer) {
var _a2;
if (!importer)
return;
if (!scopedPackagePattern.test(source))
return;
if (!nativeRecord.has(source)) {
nativeRecord = new Map([...nativeRecord, ...await resolveNativeRecord(source, importer) ?? []]);
}
const nativeItem = nativeRecord.get(source);
if (!nativeItem)
return;
if (((_a2 = options.ignore) == null ? void 0 : _a2.call(options, source)) === false) {
nativeItem.ignore = true;
return;
}
const nativeFilename = path.posix.join(output, source + NativeExt);
const interopFilename = path.posix.join(output, source + InteropExt);
if (!nativesMap.get(source)) {
ensureDir(path.dirname(interopFilename));
fs.writeFileSync(
interopFilename,
getInteropSnippet(source, `./${withDistAssetBase(source + NativeExt)}`)
);
nativesMap.set(source, {
status: "resolved",
nativeFilename,
interopFilename,
native: nativeItem
});
}
return { id: interopFilename };
}
};
modifyAlias(config, [alias]);
modifyOptimizeDeps(config, [...nativeRecord.keys()]);
},
async buildEnd(error) {
var _a;
if (error)
return;
if (options.webpack) {
for (const item of nativesMap) {
const [name, native2] = item;
if (native2.status === "built")
continue;
if (native2.native.ignore)
continue;
try {
await webpackBundle(name, output, options.webpack);
if (options.forceCopyIfUnbuilt) {
await forceCopyNativeFilesIfUnbuilt(
native2,
output,
((_a = options.webpack[loader1]) == null ? void 0 : _a.outputAssetBase) ?? outputAssetBase
);
}
native2.status = "built";
} catch (error2) {
console.error(`
${TAG}`, error2);
process.exit(1);
}
}
}
}
};
}
function modifyAlias(config, aliases) {
var _a;
config.resolve ?? (config.resolve = {});
(_a = config.resolve).alias ?? (_a.alias = []);
if (Object.prototype.toString.call(config.resolve.alias) === "[object Object]") {
config.resolve.alias = Object.entries(config.resolve.alias).reduce((memo, [find, replacement]) => memo.concat({ find, replacement }), []);
}
const aliasArray = config.resolve.alias;
aliasArray.push(...aliases);
}
function modifyOptimizeDeps(config, exclude) {
var _a;
config.optimizeDeps ?? (config.optimizeDeps = {});
(_a = config.optimizeDeps).exclude ?? (_a.exclude = []);
for (const str of exclude) {
if (!config.optimizeDeps.exclude.includes(str)) {
config.optimizeDeps.exclude.push(str);
}
}
}
async function webpackBundle(name, output, webpackOpts) {
var _a;
webpackOpts[loader1] ?? (webpackOpts[loader1] = {});
const { validate, webpack } = cjs.require("webpack");
const assetBase = (_a = webpackOpts[loader1]).outputAssetBase ?? (_a.outputAssetBase = outputAssetBase);
return new Promise(async (resolve, reject) => {
let options = {
mode: "none",
target: "node14",
entry: { [name]: name },
output: {
library: {
type: "commonjs2"
},
path: output,
filename: "[name]" + NativeExt
},
module: {
// @see https://github.com/electron/forge/blob/v7.4.0/packages/template/webpack-typescript/tmpl/webpack.rules.ts
rules: [
// Add support for native node modules
{
// We're specifying native_modules in the test because the asset relocator loader generates a
// "fake" .node file which is really a cjs file.
test: new RegExp(`${assetBase}[/\\\\].+\\.node$`),
use: {
loader: cjs.require.resolve("node-loader"),
options: webpackOpts["node-loader"]
}
},
{
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
parser: { amd: false },
use: {
loader: cjs.require.resolve("@vercel/webpack-asset-relocator-loader"),
options: webpackOpts[loader1]
}
}
]
}
};
if (webpackOpts.config) {
options = await webpackOpts.config(options) ?? options;
}
try {
validate(options);
} catch (error) {
reject(COLOURS.red(error.message));
return;
}
webpack(options).run((error, stats) => {
var _a2;
if (error) {
reject(error);
return;
}
if (stats == null ? void 0 : stats.hasErrors()) {
const errorMsg = (_a2 = stats.toJson().errors) == null ? void 0 : _a2.map((msg) => msg.message).join("\n");
if (errorMsg) {
reject(COLOURS.red(errorMsg));
return;
}
}
console.log(`${TAG}`, name, COLOURS.green("build success"));
resolve(null);
});
});
}
async function forceCopyNativeFilesIfUnbuilt(resolvedNative, output, assetBase) {
const { nativeFilename } = resolvedNative;
const { name: nativeName, path: nativeRoot, nativeFiles } = resolvedNative.native;
const nativeOutput = path.posix.join(output, assetBase);
const nativeNodeModules = path.posix.join(output, "node_modules");
const exists = nativeFiles.some((file) => fs.existsSync(path.join(nativeOutput, file)));
if (!exists) {
const nativeDest = path.posix.join(nativeNodeModules, nativeName);
copy(nativeRoot, nativeDest);
const dependencies = await flatDependencies(nativeRoot);
for (const dep of dependencies) {
copy(dep.src, path.join(nativeNodeModules, dep.name));
}
let relativePath = path.posix.relative(path.dirname(nativeFilename), nativeDest);
if (!relativePath.startsWith(".")) {
relativePath = `./${relativePath}`;
}
fs.writeFileSync(
path.join(nativeFilename),
`
// This is a native module that cannot be built correctly.
module.exports = require("${relativePath}");
`.trim()
);
}
}
export {
native as default
};