@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)
218 lines (214 loc) • 7.5 kB
JavaScript
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/index.ts
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";
// src/parseCompilerLog.ts
import { EOL } from "node:os";
var ruler = "\u2014".repeat(80);
var fileAndRangeRegex = /(.+):(\d+):(\d+)(-(\d+)(:(\d+))?)?$/;
var codeRegex = /^ {2,}([0-9]+| +|\.) (│|┆)/;
var 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;
}
function parseCompilerLog(log) {
const lines = log.split(EOL).filter(Boolean);
if (lines[lines.length - 1]?.startsWith("#Done(")) {
let foundError = false;
let path2;
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;
path2 = lines?.[i + 1]?.trim();
const match = path2?.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("\u2506", "|").replace("\u2502", "|");
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: path2
};
}
}
return null;
}
// src/index.ts
var logPrefix = chalk.cyan("[@jihchi/vite-plugin-rescript]");
async function launchReScript(watch, silent) {
const cmd = watch ? "rescript build -with-deps -w" : "rescript build -with-deps";
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(">>>> Finish compiling")) {
compileOnce(true);
}
}
const { stdout, stderr } = result;
stdout?.on("data", dataListener);
stderr?.on("data", dataListener);
if (watch) {
await new Promise((resolve2) => {
compileOnce = resolve2;
});
} 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;
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);
}
},
config: (userConfig) => ({
build: {
// If the build watcher is enabled (adding watch config would automatically enable it),
// exclude rescript files since recompilation should be based on the generated JS files.
watch: userConfig.build?.watch ? { exclude: ["**/*.res", "**/*.resi"] } : null
},
server: {
watch: {
// Ignore rescript files when watching since they may occasionally trigger hot update
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 });
}
);
},
// Hook that resolves `.bs.js` imports to their `.res` counterpart
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 dirname2 = path.dirname(importer);
try {
__require.resolve(source, { paths: [dirname2] });
return null;
} catch (err) {
}
const resFile = source.replace(suffixRegex, ".res");
const id = path.join(dirname2, 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 };
},
// Hook that loads the generated `.bs.js` file from `lib/es6` for ReScript files
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;
}
};
}
export {
createReScriptPlugin as default
};