@jihchi/vite-plugin-rescript
Version:
[![Workflows - CI][workflows-ci-shield]][workflows-ci-url] [![npm package][npm-package-shield]][npm-package-url] ![npm download per month][npm-download-shield] [![npm license][npm-licence-shield]](./LICENSE)
219 lines (215 loc) • 7.79 kB
JavaScript
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const node_fs = __toESM(require("node:fs"));
const node_fs_promises = __toESM(require("node:fs/promises"));
const node_path = __toESM(require("node:path"));
const chalk = __toESM(require("chalk"));
const execa = __toESM(require("execa"));
const npm_run_path = __toESM(require("npm-run-path"));
const node_os = __toESM(require("node:os"));
//#region src/parseCompilerLog.ts
const ruler = "—".repeat(80);
const fileAndRangeRegex = /(.+):(\d+):(\d+)(-(\d+)(:(\d+))?)?$/;
const codeRegex = /^ {2,}([0-9]+| +|\.) (│|┆)/;
const warningErrorRegex = /Warning number \d+ \(configured as error\)/;
function isErrorLine(line) {
if (line?.startsWith(" We've found a bug for you!")) return true;
if (line?.startsWith(" Syntax error!")) return true;
if (line && warningErrorRegex.test(line)) return true;
return false;
}
/**
* Parses the .compiler.log, returning the first error or null if no errors were found.
* @param log - The log file text.
* @returns Error object to send to the client or null.
*/
function parseCompilerLog(log) {
const lines = log.split(node_os.EOL).filter(Boolean);
if (lines[lines.length - 1]?.startsWith("#Done(")) {
let foundError = false;
let path;
let startLine = 0;
const messages = [];
const frame = [ruler];
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
if (isErrorLine(line)) {
if (foundError) break;
foundError = true;
path = lines?.[i + 1]?.trim();
const match = path?.match(fileAndRangeRegex);
if (match) startLine = Number(match[2]);
i += 1;
} else if (!foundError) {} else if (line?.startsWith(" Warning number ")) break;
else if (line?.startsWith("#Done(")) break;
else {
const match = line?.match(codeRegex);
if (match) {
let codeFrameLine = line?.replace("┆", "|").replace("│", "|");
if (Number(match[1]) === startLine) codeFrameLine = `> ${codeFrameLine?.substring(2)}`;
if (codeFrameLine) frame.push(codeFrameLine);
} else if (line?.startsWith(" ")) messages.push(line.trim());
}
}
if (foundError) return {
message: messages.join("\n"),
frame: `${frame.join("\n")}`,
stack: "",
id: path
};
}
return null;
}
//#endregion
//#region src/index.ts
const logPrefix = chalk.default.cyan("[@jihchi/vite-plugin-rescript]");
async function launchReScript(watch, silent, rewatch, buildArgs) {
let cmd;
let finishSignal;
if (rewatch) {
cmd = watch ? "rewatch watch" : "rewatch build";
finishSignal = "Finished initial compilation";
} else {
cmd = watch ? "rescript build -with-deps -w" : "rescript build -with-deps";
finishSignal = ">>>> Finish compiling";
}
if (buildArgs) cmd += ` ${buildArgs}`;
const result = (0, execa.execaCommand)(cmd, {
env: (0, npm_run_path.npmRunPathEnv)(),
extendEnv: true,
shell: true,
windowsHide: false,
cwd: process.cwd()
});
let compileOnce = (_value) => {};
function dataListener(chunk) {
const output = chunk.toString().trimEnd();
if (!silent) console.log(logPrefix, output);
if (watch && output.includes(finishSignal)) compileOnce(true);
}
const { stdout, stderr } = result;
stdout?.on("data", dataListener);
stderr?.on("data", dataListener);
if (watch) await new Promise((resolve) => {
compileOnce = resolve;
});
else await result;
return { shutdown() {
if (!result.killed) result.kill();
} };
}
function createReScriptPlugin(config) {
let root;
let usingLoader = false;
let childProcessReScript;
const output = config?.loader?.output ?? "./lib/es6";
const suffix = config?.loader?.suffix ?? ".bs.js";
const suffixRegex = new RegExp(`${suffix.replace(".", "\\.")}$`);
const silent = config?.silent ?? false;
const rewatch = config?.rewatch ?? false;
const buildArgs = config?.buildArgs ?? "";
return {
name: "@jihchi/vite-plugin-rescript",
enforce: "pre",
async configResolved(resolvedConfig) {
root = resolvedConfig.root;
const { build, command, inlineConfig } = resolvedConfig;
const isOnlyDevServerLaunching = command === "serve" && !Object.hasOwn(inlineConfig, "preview");
const isBuildForProduction = command === "build";
const needReScript = isOnlyDevServerLaunching || isBuildForProduction;
const isLocked = (0, node_fs.existsSync)(node_path.resolve("./.bsb.lock"));
const watch = !isLocked && (command === "serve" || Boolean(build.watch));
if (needReScript) childProcessReScript = await launchReScript(watch, silent, rewatch, buildArgs);
},
config: (userConfig) => ({
build: { watch: userConfig.build?.watch ? { exclude: ["**/*.res", "**/*.resi"] } : null },
server: { watch: { ignored: ["**/*.res", "**/*.resi"] } }
}),
configureServer(server) {
node_fs_promises.readFile(node_path.resolve("./lib/bs/.compiler.log"), "utf8").then((data) => {
const log = data.toString();
const err = parseCompilerLog(log);
if (err) server.hot.send({
type: "error",
err
});
});
},
async resolveId(source, importer, options) {
if (source.endsWith(".res")) usingLoader = true;
if (options.isEntry || !importer) return null;
if (!importer.endsWith(".res")) return null;
if (!source.endsWith(suffix)) return null;
if (node_path.isAbsolute(source)) return null;
const dirname = node_path.dirname(importer);
try {
require.resolve(source, { paths: [dirname] });
return null;
} catch {}
const resFile = source.replace(suffixRegex, ".res");
const id = node_path.join(dirname, resFile);
const resolution = await this.resolve(resFile, importer, {
skipSelf: true,
...options
});
if (!resolution || resolution.external) return resolution;
if (resolution.id !== resFile) return resolution;
return {
...resolution,
id
};
},
async load(id) {
if (!id.endsWith(".res")) return null;
const relativePath = node_path.relative(root, id);
const filePath = node_path.resolve(output, relativePath).replace(/\.res$/, suffix);
this.addWatchFile(filePath);
return { code: await node_fs_promises.readFile(filePath, "utf8") };
},
async handleHotUpdate({ file, read, server }) {
if (usingLoader && file.endsWith(suffix)) {
const lib = node_path.resolve(output);
const relativePath = node_path.relative(lib, file);
if (relativePath.startsWith("..")) return;
const resFile = relativePath.replace(suffixRegex, ".res");
const id = node_path.join(root, resFile);
const moduleNode = server.moduleGraph.getModuleById(id);
if (moduleNode) return [moduleNode];
} else if (file.endsWith(".compiler.log")) {
const log = await read();
const err = parseCompilerLog(log);
if (err) server.hot.send({
type: "error",
err
});
}
return;
},
async closeBundle() {
childProcessReScript?.shutdown();
return;
}
};
}
//#endregion
module.exports = createReScriptPlugin;