From 83933feb59cf53d3f914e8863048f9872286526f Mon Sep 17 00:00:00 2001 From: "Raulo Erwan." Date: Tue, 10 Mar 2026 19:06:44 +0100 Subject: [PATCH] tech: enable watch mode & esbuild server in dev mode --- esbuild.dev.config.ts | 160 ++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + public/main.js | 3 + src/commands/http.js | 66 +++++++++-------- 4 files changed, 201 insertions(+), 30 deletions(-) create mode 100644 esbuild.dev.config.ts diff --git a/esbuild.dev.config.ts b/esbuild.dev.config.ts new file mode 100644 index 00000000..75e4a303 --- /dev/null +++ b/esbuild.dev.config.ts @@ -0,0 +1,160 @@ +// Import Node.js Dependencies +import fs from "node:fs"; +import fsAsync from "node:fs/promises"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// Import Third-party Dependencies +import { getBuildConfiguration } from "@nodesecure/documentation-ui/node"; +import * as i18n from "@nodesecure/i18n"; +import esbuild from "esbuild"; +import router from "find-my-way"; +import open from "open"; +import sirv from "sirv"; + +// Import Internal Dependencies +import english from "./i18n/english.js"; +import french from "./i18n/french.js"; +import { context as alsContext } from "./workspaces/server/src/ALS.ts"; +import { ViewBuilder } from "./workspaces/server/src/ViewBuilder.class.ts"; +import { cache } from "./workspaces/server/src/cache.ts"; +import * as bundle from "./workspaces/server/src/endpoints/bundle.ts"; +import * as config from "./workspaces/server/src/endpoints/config.ts"; +import * as data from "./workspaces/server/src/endpoints/data.ts"; +import * as flags from "./workspaces/server/src/endpoints/flags.ts"; +import * as locali18n from "./workspaces/server/src/endpoints/i18n.ts"; +import * as npmDownloads from "./workspaces/server/src/endpoints/npm-downloads.ts"; +import * as scorecard from "./workspaces/server/src/endpoints/ossf-scorecard.ts"; +import * as report from "./workspaces/server/src/endpoints/report.ts"; +import * as root from "./workspaces/server/src/endpoints/root.ts"; +import * as search from "./workspaces/server/src/endpoints/search.ts"; +import { logger } from "./workspaces/server/src/logger.ts"; +import { WebSocketServerInstanciator } from "./workspaces/server/src/websocket/index.ts"; + +// CONSTANTS +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const kPublicDir = path.join(__dirname, "public"); +const kOutDir = path.join(__dirname, "dist"); +const kImagesDir = path.join(kPublicDir, "img"); +const kNodeModulesDir = path.join(__dirname, "node_modules"); +const kComponentsDir = path.join(kPublicDir, "components"); +const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json"); + +const kDevPort = Number(process.env.DEV_PORT ?? 8080); + +await i18n.getLocalLang(); +await i18n.extendFromSystemPath(path.join(__dirname, "i18n")); + +const imagesFiles = await fsAsync.readdir(kImagesDir); + +await Promise.all([ + ...imagesFiles + .map((name) => fsAsync.copyFile(path.join(kImagesDir, name), path.join(kOutDir, name))), + fsAsync.copyFile(path.join(kPublicDir, "favicon.ico"), path.join(kOutDir, "favicon.ico")) +]); + +const buildContext = await esbuild.context({ + entryPoints: [ + path.join(kPublicDir, "main.js"), + path.join(kPublicDir, "main.css"), + path.join(kNodeModulesDir, "highlight.js", "styles", "github.css"), + ...getBuildConfiguration().entryPoints + ], + + loader: { + ".jpg": "file", + ".png": "file", + ".woff": "file", + ".woff2": "file", + ".eot": "file", + ".ttf": "file", + ".svg": "file" + }, + platform: "browser", + bundle: true, + sourcemap: true, + treeShaking: true, + outdir: kOutDir +}); + +await buildContext.watch(); + +const { hosts: esbuildHosts, port: esbuildPort } = await buildContext.serve({ + servedir: kOutDir +}); + +const dataFilePath = fs.existsSync(kDefaultPayloadPath) ? kDefaultPayloadPath : undefined; + +if (dataFilePath === undefined) { + cache.startFromZero = true; +} + +const store = { + i18n: { english: { ui: english.ui }, french: { ui: french.ui } }, + viewBuilder: new ViewBuilder({ + autoReload: true, + projectRootDir: __dirname, + componentsDir: kComponentsDir + }), + dataFilePath +}; + +const serving = sirv(kOutDir, { dev: true }); + +const apiRouter = router({ + ignoreTrailingSlash: true, + defaultRoute: (req: http.IncomingMessage, res: http.ServerResponse) => { + if (req.url === "/esbuild") { + const proxyReq = http.request( + { hostname: esbuildHosts[0], port: esbuildPort, path: req.url, method: req.method, headers: req.headers }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode!, proxyRes.headers); + proxyRes.pipe(res); + } + ); + + proxyReq.on("error", (err) => { + console.error(`[proxy/esbuild] ${err.message}`); + res.writeHead(502); + res.end("Bad Gateway"); + }); + + req.pipe(proxyReq); + + return; + } + + serving(req, res, () => { + res.writeHead(404); + res.end("Not Found"); + }); + } +}); + +// same as workspaces/server/src/endpoints/index.ts --- +apiRouter.get("/", root.get); +apiRouter.get("/data", data.get); +apiRouter.get("/config", config.get); +apiRouter.put("/config", config.save); +apiRouter.get("/i18n", locali18n.get); +apiRouter.get("/search/:packageName", search.get); +apiRouter.get("/search-versions/:packageName", search.versions); +apiRouter.get("/flags", flags.getAll); +apiRouter.get("/flags/description/:title", flags.get); +apiRouter.get("/bundle/:packageName", bundle.get); +apiRouter.get("/bundle/:packageName/:version", bundle.get); +apiRouter.get("/downloads/:packageName", npmDownloads.get); +apiRouter.get("/scorecard/:org/:packageName", scorecard.get); +apiRouter.post("/report", report.post); + +http.createServer((req, res) => alsContext.run(store, () => apiRouter.lookup(req, res))) + .listen(kDevPort, () => { + console.log(`Dev server: http://localhost:${kDevPort}`); + open(`http://localhost:${kDevPort}`); + }); + +new WebSocketServerInstanciator({ cache, logger }); + +console.log("Watching..."); diff --git a/package.json b/package.json index 2c4f14e9..dad44dec 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "lint-fix": "npm run lint -- --fix", "prepublishOnly": "rimraf ./dist && npm run build && pkg-ok", "build": "npm run build:front && npm run build:workspaces", + "build:dev": "npm run build:front:dev && npm run build:workspaces", "build:front": "node ./esbuild.config.js", + "build:front:dev": "node --experimental-strip-types ./esbuild.dev.config.ts", "build:workspaces": "npm run build --ws --if-present", "test": "npm run test:cli && npm run lint && npm run lint:css", "test:cli": "node --no-warnings --test test/**/*.test.js", diff --git a/public/main.js b/public/main.js index 6ad77cc1..f7bdadee 100644 --- a/public/main.js +++ b/public/main.js @@ -291,3 +291,6 @@ function onSettingsSaved(defaultConfig = null) { networkView.classList.remove("locked"); }); } + +new EventSource("/esbuild").addEventListener("change", () => location.reload()); + diff --git a/src/commands/http.js b/src/commands/http.js index 26d0a8eb..07853475 100644 --- a/src/commands/http.js +++ b/src/commands/http.js @@ -1,18 +1,18 @@ // Import Node.js Dependencies +import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import crypto from "node:crypto"; // Import Third-party Dependencies -import open from "open"; -import * as SemVer from "semver"; import * as i18n from "@nodesecure/i18n"; import { + buildServer, cache, logger, - buildServer, WebSocketServerInstanciator } from "@nodesecure/server"; +import open from "open"; +import * as SemVer from "semver"; // Import Internal Dependencies import english from "../../i18n/english.js"; @@ -51,37 +51,43 @@ export async function start( cache.prefix = crypto.randomBytes(4).toString("hex"); } - const httpServer = buildServer(dataFilePath, { - port: httpPort, - hotReload: enableDeveloperMode, - runFromPayload, - projectRootDir: kProjectRootDir, - componentsDir: kComponentsDir, - i18n: { - english, - french - } - }); + if (enableDeveloperMode) { + // todo: ping/warn if dev server is not up & running + open("http://127.0.0.1:8080"); + } + else { + const httpServer = buildServer(dataFilePath, { + port: httpPort, + hotReload: enableDeveloperMode, + runFromPayload, + projectRootDir: kProjectRootDir, + componentsDir: kComponentsDir, + i18n: { + english, + french + } + }); - httpServer.listen(httpPort, async() => { - const link = `http://localhost:${httpServer.address().port}`; - console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link)); + httpServer.listen(httpPort, async() => { + const link = `http://localhost:${httpServer.address().port}`; + console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link)); - open(link); - }); + open(link); + }); - new WebSocketServerInstanciator({ - cache, - logger - }); + new WebSocketServerInstanciator({ + cache, + logger + }); - for (const eventName of ["SIGINT", "SIGTERM"]) { - process.on(eventName, () => { - httpServer.close(); + for (const eventName of ["SIGINT", "SIGTERM"]) { + process.on(eventName, () => { + httpServer.close(); - console.log(kleur.red().bold(`${eventName} signal received.`)); - process.exit(0); - }); + console.log(kleur.red().bold(`${eventName} signal received.`)); + process.exit(0); + }); + } } }