Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions build-scripts/download-lua-ls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env node
/**
* Downloads the appropriate lua-language-server binary for the current
* (or target) platform and extracts it into resources/lua-language-server/.
*
* Usage:
* node build-scripts/download-lua-ls.js
* node build-scripts/download-lua-ls.js --version 3.13.6
* node build-scripts/download-lua-ls.js --platform win32 --arch x64
*
* The script is intentionally self-contained (no extra npm deps beyond
* Node built-ins + node-fetch which is already a project dependency).
*/

"use strict";

const https = require("https");
const http = require("http");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { execSync } = require("child_process");

// ── Configuration ────────────────────────────────────────────────────────────

const LUA_LS_VERSION =
parseArg("--version") || process.env.LUA_LS_VERSION || "3.13.6";

const platform = parseArg("--platform") || process.platform; // win32 | darwin | linux
const arch = parseArg("--arch") || process.arch; // x64 | arm64

const OUTPUT_DIR = path.resolve(
__dirname,
"..",
"resources",
"lua-language-server",
);

// ── Platform → asset name mapping ───────────────────────────────────────────

/** Maps Node.js platform/arch to the GitHub release asset suffix. */
function getAssetName(platform, arch, version) {
const platformMap = {
win32: { x64: `win32-x64` },
darwin: { x64: `darwin-x64`, arm64: `darwin-arm64` },
linux: { x64: `linux-x64` },
};

const key = (platformMap[platform] || {})[arch];
if (!key) {
throw new Error(
`Unsupported platform/arch combination: ${platform}/${arch}`,
);
}

const ext = platform === "win32" ? "zip" : "tar.gz";
return `lua-language-server-${version}-${key}.${ext}`;
}

/** Returns the download URL for the given version and asset name. */
function getDownloadUrl(version, assetName) {
return `https://github.com/LuaLS/lua-language-server/releases/download/${version}/${assetName}`;
}

// ── Helpers ──────────────────────────────────────────────────────────────────

function parseArg(name) {
const idx = process.argv.indexOf(name);
return idx !== -1 ? process.argv[idx + 1] : null;
}

function mkdirp(dir) {
fs.mkdirSync(dir, { recursive: true });
}

/** Follow HTTP redirects and return the final response. */
function fetch(url) {
return new Promise((resolve, reject) => {
const protocol = url.startsWith("https") ? https : http;
protocol.get(url, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
fetch(res.headers.location).then(resolve).catch(reject);
} else if (res.statusCode !== 200) {
reject(
new Error(
`HTTP ${res.statusCode} while downloading ${url}`,
),
);
} else {
resolve(res);
}
}).on("error", reject);
});
}

/** Download url → destPath, showing a simple progress counter. */
async function download(url, destPath) {
console.log(` Downloading: ${url}`);
const res = await fetch(url);
const total = parseInt(res.headers["content-length"] || "0", 10);
let received = 0;

return new Promise((resolve, reject) => {
const out = fs.createWriteStream(destPath);
res.on("data", (chunk) => {
received += chunk.length;
if (total > 0) {
const pct = Math.round((received / total) * 100);
process.stdout.write(`\r Progress: ${pct}%`);
}
});
res.pipe(out);
out.on("finish", () => {
process.stdout.write("\n");
resolve();
});
out.on("error", reject);
res.on("error", reject);
});
}

/** Extract a .tar.gz archive using the system tar command. */
function extractTarGz(archivePath, destDir) {
mkdirp(destDir);
execSync(`tar -xzf "${archivePath}" -C "${destDir}"`);
}

/** Extract a .zip archive using the system unzip / PowerShell. */
function extractZip(archivePath, destDir) {
mkdirp(destDir);
if (process.platform === "win32") {
execSync(
`powershell -Command "Expand-Archive -Force '${archivePath}' '${destDir}'"`,
);
} else {
execSync(`unzip -o "${archivePath}" -d "${destDir}"`);
}
}

// ── Main ─────────────────────────────────────────────────────────────────────

async function main() {
console.log(
`\nlua-language-server downloader (v${LUA_LS_VERSION} ${platform}/${arch})\n`,
);

const assetName = getAssetName(platform, arch, LUA_LS_VERSION);
const url = getDownloadUrl(LUA_LS_VERSION, assetName);

// Check if already present
const binaryName =
platform === "win32"
? "lua-language-server.exe"
: "lua-language-server";
const binaryPath = path.join(OUTPUT_DIR, "bin", binaryName);

if (fs.existsSync(binaryPath)) {
console.log(` Already installed: ${binaryPath}`);
console.log(" Delete resources/lua-language-server/ to force re-download.");
return;
}

// Download
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lua-ls-"));
const archivePath = path.join(tmpDir, assetName);

try {
await download(url, archivePath);

// Extract into OUTPUT_DIR
console.log(` Extracting to: ${OUTPUT_DIR}`);
if (archivePath.endsWith(".tar.gz")) {
extractTarGz(archivePath, OUTPUT_DIR);
} else {
extractZip(archivePath, OUTPUT_DIR);
}

if (!fs.existsSync(binaryPath)) {
throw new Error(
`Extraction succeeded but binary not found at: ${binaryPath}`,
);
}

// Make executable on POSIX
if (platform !== "win32") {
fs.chmodSync(binaryPath, 0o755);
}

console.log(` ✓ lua-language-server ready: ${binaryPath}`);
} finally {
// Clean up temp dir
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}

main().catch((err) => {
console.error("\n✗ Failed to download lua-language-server:", err.message);
process.exit(1);
});
9 changes: 9 additions & 0 deletions electron-builder-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ const config = {
from: "src/renderer/assets/**/*",
to: "assets",
},
{
// lua-language-server binary — downloaded by `node build-scripts/download-lua-ls.js`
// before running electron-builder. The directory is only included when it exists
// (i.e. the download script has been run); builds without it still work but LuaLS
// features will be unavailable at runtime.
from: "resources/lua-language-server",
to: "lua-language-server",
filter: ["**/*"],
},
],
files: ["**/*"],
win: {
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,14 @@
"yauzl": "^3.2.0"
},
"scripts": {
"download-lua-ls": "node build-scripts/download-lua-ls.js",
"e:builder": "cross-env VITE_BUILD_TARGET=electron NOTARIZE=true electron-builder --config electron-builder-config.js --publish onTagOrDraft",
"e:builder:nightly": "cross-env VITE_BUILD_ENV=nightly VITE_BUILD_TARGET=electron NOTARIZE=true DEBUG=electron-notarize* electron-builder --config electron-builder-config.js --publish never",
"e:builder:local": "cross-env VITE_BUILD_ENV=nightly VITE_BUILD_TARGET=electron DEBUG=electron-builder DEBUG=electron-notarize* electron-builder --config electron-builder-config.js --publish never",
"export": "cross-env VITE_BUILD_ENV=production run-s s:build e:builder",
"export:nightly": "run-s s:build e:builder:nightly",
"export:alpha": "cross-env VITE_BUILD_ENV=alpha run-s s:build e:builder",
"export:local": "run-s clean-up s:build e:builder:local",
"export": "cross-env VITE_BUILD_ENV=production run-s download-lua-ls s:build e:builder",
"export:nightly": "run-s download-lua-ls s:build e:builder:nightly",
"export:alpha": "cross-env VITE_BUILD_ENV=alpha run-s download-lua-ls s:build e:builder",
"export:local": "run-s clean-up download-lua-ls s:build e:builder:local",
"clean-up": "rm -rf dist && rm -rf dist-web && rm -rf build",
"rebuild": "./node_modules/.bin/electron-rebuild.cmd",
"s:build": "electron-vite build",
Expand Down
31 changes: 31 additions & 0 deletions src/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
} from "./src/profiles";
import { fetchReleaseNotes, fetchUrlJSON } from "./src/fetch";
import { getLatestVideo } from "./src/youtube";
import { getLuaLanguageServer } from "./src/lua-language-server";

log.info("App starting...");
log.info("BUILD ENVS:", import.meta.env);
Expand All @@ -94,6 +95,9 @@ log.info("BUILD ENVS:", import.meta.env);
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

// LuaLS WebSocket proxy port (0 = not started / binary not found)
let luaLsPort = 0;

// Bootloader detection service state
let bootloaderDetectionTimeout: NodeJS.Timeout | null = null;
let bootloaderDetectionRunning = false;
Expand Down Expand Up @@ -356,6 +360,28 @@ if (!gotTheLock) {
create_tray();
}
createWindow();

// Start the LuaLS proxy server. The port is exposed to the renderer via
// the "getLuaLsPort" IPC channel (see preload.ts).
const annotationsDir = path.resolve(
__dirname,
"..",
"..",
"renderer",
"assets",
"lua",
"annotations",
);
getLuaLanguageServer()
.start({ annotationsDir })
.then((port) => {
luaLsPort = port;
log.info(`[LuaLS] Proxy started on port ${port}`);
})
.catch((err) => {
log.warn("[LuaLS] Failed to start proxy:", err);
});

protocol.handle("package", (req) => {
let requestPath = req.url.substring("package://".length);

Expand Down Expand Up @@ -1001,6 +1027,10 @@ ipcMain.on("get-app-path", (event) => {
event.returnValue = app.getAppPath();
});

ipcMain.on("getLuaLsPort", (event) => {
event.returnValue = luaLsPort;
});

ipcMain.on("analytics_uuid", (event) => {
event.returnValue = store.get("userId");
});
Expand Down Expand Up @@ -1085,4 +1115,5 @@ app.on("activate", () => {
app.on("before-quit", (evt) => {
log.info("before-quit evt", evt);
app.quitting = true;
getLuaLanguageServer().stop();
});
3 changes: 3 additions & 0 deletions src/electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ contextBridge.exposeInMainWorld("electron", {
appLoaded: () => appLoadedPromiseResolve(undefined),
showQuitDialog: (callback) => ipcRenderer.on("showQuitDialog", callback),
quitDialogResult: (result) => ipcRenderer.send("quitDialogResult", result),
luaLanguageServer: {
getPort: () => ipcRenderer.sendSync("getLuaLsPort") as number,
},
});

let appLoadedPromiseResolve: (any) => any;
Expand Down
Loading