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)

218 lines (214 loc) 7.5 kB
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 };