206 lines
5.7 KiB
TypeScript
206 lines
5.7 KiB
TypeScript
import astMatcher from "ast-matcher";
|
|
import MagicString from "magic-string";
|
|
// @ts-ignore
|
|
import loady from "loady";
|
|
// @ts-ignore
|
|
import nodeGybBuild from "node-gyp-build";
|
|
// @ts-ignore
|
|
import nodeGybBuildOptional from "node-gyp-build-optional-packages";
|
|
import fs from "fs";
|
|
import type { PluginContext } from "rollup";
|
|
import type { Plugin } from "vite";
|
|
import path from "path";
|
|
|
|
const loaderFunction = `
|
|
function loadNativeModuleTemp (module, data) {
|
|
const loady = require("loady");
|
|
const tempDir = require("os").tmpdir();
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const loadPath = path.join(tempDir, module);
|
|
const outputPath = path.join(loadPath, "build", "Release");
|
|
const modulePath = path.join(outputPath, module + ".node");
|
|
|
|
fs.mkdirSync(outputPath, { recursive: true });
|
|
fs.writeFileSync(modulePath, Buffer.from(data, "base64"));
|
|
|
|
if (process.pkg) {
|
|
process.pkg = undefined;
|
|
}
|
|
|
|
loady(module, loadPath);
|
|
}`;
|
|
|
|
type Edit = [number, number];
|
|
type AstNode = { start: number; end: number };
|
|
|
|
export default function bundleNativeModulesPlugin() {
|
|
return {
|
|
name: "bundle-native-modules",
|
|
transform(src, id, ast: any) {
|
|
if (!/\.(c?js)$/.test(id)) {
|
|
return null;
|
|
}
|
|
const magicString = new MagicString(src);
|
|
|
|
const parse = (
|
|
code: string,
|
|
source = code
|
|
): ReturnType<PluginContext["parse"]> => {
|
|
try {
|
|
return this.parse(code, undefined);
|
|
} catch (error) {
|
|
(error as Error).message += ` in ${source}`;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
astMatcher.setParser(parse);
|
|
|
|
if (!ast) {
|
|
try {
|
|
ast = parse(src);
|
|
} catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
const edits: Edit[] = [];
|
|
|
|
/*
|
|
Copied from https://github.com/sastan/rollup-plugin-define/blob/main/src/define.ts
|
|
*/
|
|
function markEdited(node: AstNode, edits: Edit[]): number | false {
|
|
for (const [start, end] of edits) {
|
|
if (
|
|
(start <= node.start && node.start < end) ||
|
|
(start < node.end && node.end <= end)
|
|
) {
|
|
return false; // Already edited
|
|
}
|
|
}
|
|
|
|
// Not edited
|
|
return edits.push([node.start, node.end]);
|
|
}
|
|
|
|
for (const matchString of ["require('loady')(__str_aName, __any)"]) {
|
|
const findLoady = astMatcher(matchString);
|
|
const loadyMatches = findLoady(ast);
|
|
|
|
if (loadyMatches?.length) {
|
|
for (const match of loadyMatches) {
|
|
if (markEdited(match.node, edits)) {
|
|
const modulePath = loady.resolve(match.match.aName, id);
|
|
const moduleFile = fs.readFileSync(modulePath);
|
|
const moduleB64 = moduleFile.toString("base64");
|
|
magicString.overwrite(
|
|
match.node.start,
|
|
match.node.end,
|
|
`loadNativeModuleTemp('${match.match.aName}', '${moduleB64}')`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const matchString of [
|
|
"require('node-gyp-build')(__any)",
|
|
"loadNAPI(__any)",
|
|
"loadNAPI__default[__any](__any)",
|
|
]) {
|
|
const findNodeBuildGyp = astMatcher(matchString);
|
|
const nodeBuildGypMatches = findNodeBuildGyp(ast);
|
|
|
|
if (nodeBuildGypMatches?.length) {
|
|
for (const match of nodeBuildGypMatches) {
|
|
if (markEdited(match.node, edits)) {
|
|
let modulePath;
|
|
|
|
try {
|
|
modulePath = nodeGybBuild.path(path.dirname(id));
|
|
} catch {}
|
|
|
|
if (!modulePath) {
|
|
try {
|
|
modulePath = nodeGybBuildOptional.path(path.dirname(id));
|
|
} catch {}
|
|
let parentDir = path.dirname(id);
|
|
do {
|
|
parentDir = path.dirname(parentDir);
|
|
} while (
|
|
!fs.existsSync(path.join(parentDir, "package.json")) &&
|
|
parentDir !== "/"
|
|
);
|
|
|
|
try {
|
|
modulePath = nodeGybBuildOptional.path(parentDir);
|
|
} catch {}
|
|
}
|
|
|
|
if (!modulePath) {
|
|
throw new Error(`Could not process native module for ${id}`);
|
|
}
|
|
|
|
let moduleName = "";
|
|
|
|
for (const part of modulePath
|
|
.split("node_modules")
|
|
.pop()
|
|
.split("/")
|
|
.slice(1)) {
|
|
if (part.includes(".node")) {
|
|
continue;
|
|
}
|
|
if (part === "prebuilds") {
|
|
break;
|
|
}
|
|
|
|
moduleName += part;
|
|
|
|
if (part.includes("@")) {
|
|
moduleName += "_";
|
|
}
|
|
}
|
|
|
|
const moduleFile = fs.readFileSync(modulePath);
|
|
const moduleB64 = moduleFile.toString("base64");
|
|
magicString.overwrite(
|
|
match.node.start,
|
|
match.node.end,
|
|
`loadNativeModuleTemp('${moduleName}', '${moduleB64}')`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (edits.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const findStrict = astMatcher('"use strict";');
|
|
const strictMatches = findStrict(ast);
|
|
|
|
let injectNode;
|
|
|
|
if (strictMatches?.length) {
|
|
injectNode = strictMatches[0].node;
|
|
} else {
|
|
injectNode = ast.body[0];
|
|
}
|
|
|
|
magicString.appendRight(injectNode.end + 1, loaderFunction);
|
|
|
|
return {
|
|
code: magicString.toString(),
|
|
map: magicString.generateMap({
|
|
source: src,
|
|
includeContent: true,
|
|
hires: true,
|
|
}),
|
|
};
|
|
},
|
|
} as Plugin;
|
|
}
|
|
export { bundleNativeModulesPlugin };
|