@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)
201 lines (197 loc) • 6.76 kB
JavaScript
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import * as fs from "node:fs/promises";
import * as path from "node:path";
import chalk from "chalk";
import { execaCommand } from "execa";
import { npmRunPathEnv } from "npm-run-path";
import { EOL } from "node:os";
//#region rolldown:runtime
var __require = /* @__PURE__ */ createRequire(import.meta.url);
//#endregion
//#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(EOL).filter(Boolean);
if (lines[lines.length - 1]?.startsWith("#Done(")) {
let foundError = false;
let path$1;
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$1 = lines?.[i + 1]?.trim();
const match = path$1?.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$1
};
}
return null;
}
//#endregion
//#region src/index.ts
const logPrefix = chalk.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 = execaCommand(cmd, {
env: 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 = existsSync(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) {
fs.readFile(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 (path.isAbsolute(source)) return null;
const dirname = path.dirname(importer);
try {
__require.resolve(source, { paths: [dirname] });
return null;
} catch {}
const resFile = source.replace(suffixRegex, ".res");
const id = 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 = path.relative(root, id);
const filePath = path.resolve(output, relativePath).replace(/\.res$/, suffix);
this.addWatchFile(filePath);
return { code: await fs.readFile(filePath, "utf8") };
},
async handleHotUpdate({ file, read, server }) {
if (usingLoader && file.endsWith(suffix)) {
const lib = path.resolve(output);
const relativePath = path.relative(lib, file);
if (relativePath.startsWith("..")) return;
const resFile = relativePath.replace(suffixRegex, ".res");
const id = 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
export { createReScriptPlugin as default };