UNPKG

@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
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 };