UNPKG

vite-plugin-native

Version:
347 lines (346 loc) 11.8 kB
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 };