From c04b9429e865f27cafe4490d33e0a9c7f8cd10fe 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 --- bin/index.js | 2 +- esbuild.dev.config.ts | 143 +++++++++++++++++++++ package.json | 2 + public/main.js | 3 + src/commands/http.js | 16 ++- workspaces/server/src/ViewBuilder.class.ts | 23 +--- workspaces/server/src/endpoints/index.ts | 10 +- workspaces/server/src/index.ts | 12 +- 8 files changed, 176 insertions(+), 35 deletions(-) create mode 100644 esbuild.dev.config.ts diff --git a/bin/index.js b/bin/index.js index 13e372d4..a2483035 100755 --- a/bin/index.js +++ b/bin/index.js @@ -67,7 +67,7 @@ defaultScannerCommand("from ") defaultScannerCommand("auto [spec]", { includeOutput: false, strategy: vulnera.strategies.GITHUB_ADVISORY }) .describe(i18n.getTokenSync("cli.commands.auto.desc")) .option("-k, --keep", i18n.getTokenSync("cli.commands.auto.option_keep"), false) - .option("-d, --developer", i18n.getTokenSync("cli.commands.open.option_developer"), false) + .option("--developer", i18n.getTokenSync("cli.commands.open.option_developer"), false) .action(async(spec, options) => { checkNodeSecureToken(); await commands.scanner.auto(spec, options); diff --git a/esbuild.dev.config.ts b/esbuild.dev.config.ts new file mode 100644 index 00000000..56059d36 --- /dev/null +++ b/esbuild.dev.config.ts @@ -0,0 +1,143 @@ +// 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 chokidar from "chokidar"; +import esbuild from "esbuild"; +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 { getApiRouter } from "./workspaces/server/src/endpoints/index.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 htmlWatcher = chokidar.watch(kComponentsDir, { + persistent: false, + awaitWriteFinish: true, + ignored: (path, stats) => (stats?.isFile() ?? false) && !path.endsWith(".html") +}); + +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({ + projectRootDir: __dirname, + componentsDir: kComponentsDir + }), + dataFilePath +}; + +htmlWatcher.on("change", (filePath) => { + buildContext.rebuild(); + store.viewBuilder.freeCache(filePath); +}); + +const serving = sirv(kOutDir, { dev: true }); + +function 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"); + }); +} + +const apiRouter = getApiRouter(defaultRoute); + +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..71d1e101 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:workspaces && npm run build:front:dev", "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..9b90943b 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,9 +51,15 @@ export async function start( cache.prefix = crypto.randomBytes(4).toString("hex"); } + if (enableDeveloperMode) { + const link = "http://127.0.0.1:8080"; + console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link)); + open(link); + + return; + } const httpServer = buildServer(dataFilePath, { port: httpPort, - hotReload: enableDeveloperMode, runFromPayload, projectRootDir: kProjectRootDir, componentsDir: kComponentsDir, diff --git a/workspaces/server/src/ViewBuilder.class.ts b/workspaces/server/src/ViewBuilder.class.ts index f308a293..68413d4d 100644 --- a/workspaces/server/src/ViewBuilder.class.ts +++ b/workspaces/server/src/ViewBuilder.class.ts @@ -1,12 +1,11 @@ // Import Node.js Dependencies -import path from "node:path"; import fs from "node:fs/promises"; +import path from "node:path"; // Import Third-party Dependencies -import zup from "zup"; import * as i18n from "@nodesecure/i18n"; -import chokidar from "chokidar"; import { globStream } from "glob"; +import zup from "zup"; // Import Internal Dependencies import { logger } from "./logger.ts"; @@ -24,31 +23,15 @@ export class ViewBuilder { constructor(options: ViewBuilderOptions) { const { - autoReload = false, projectRootDir, componentsDir } = options; this.projectRootDir = projectRootDir; this.componentsDir = componentsDir; - - if (autoReload) { - this.#enableWatcher(); - } - } - - async #enableWatcher() { - logger.info("[ViewBuilder] autoReload is enabled"); - - const watcher = chokidar.watch(this.componentsDir, { - persistent: false, - awaitWriteFinish: true, - ignored: (path, stats) => (stats?.isFile() ?? false) && !path.endsWith(".html") - }); - watcher.on("change", (filePath) => this.#freeCache(filePath)); } - async #freeCache( + freeCache( filePath: string ) { logger.info("[ViewBuilder] the cache has been released"); diff --git a/workspaces/server/src/endpoints/index.ts b/workspaces/server/src/endpoints/index.ts index 31955012..6fc44ad9 100644 --- a/workspaces/server/src/endpoints/index.ts +++ b/workspaces/server/src/endpoints/index.ts @@ -1,3 +1,6 @@ +// Import Node.js Dependencies +import http from "node:http"; + // Import Third-party Dependencies import router from "find-my-way"; @@ -13,9 +16,12 @@ import * as scorecard from "./ossf-scorecard.ts"; import * as locali18n from "./i18n.ts"; import * as report from "./report.ts"; -export function getApiRouter() { +type DefaultRoute = (req: http.IncomingMessage, res: http.ServerResponse) => void; + +export function getApiRouter(defaultRoute?: DefaultRoute) { const apiRouter = router({ - ignoreTrailingSlash: true + ignoreTrailingSlash: true, + defaultRoute }); apiRouter.get("/", root.get); diff --git a/workspaces/server/src/index.ts b/workspaces/server/src/index.ts index 84b7d56f..762195bc 100644 --- a/workspaces/server/src/index.ts +++ b/workspaces/server/src/index.ts @@ -1,20 +1,20 @@ // Import Node.js Dependencies import fs from "node:fs"; -import path from "node:path"; import http from "node:http"; +import path from "node:path"; // Import Third-party Dependencies import sirv from "sirv"; // Import Internal Dependencies -import { getApiRouter } from "./endpoints/index.ts"; -import { ViewBuilder } from "./ViewBuilder.class.ts"; import { context, type AsyncStoreContext, type NestedStringRecord } from "./ALS.ts"; import { cache } from "./cache.ts"; +import { getApiRouter } from "./endpoints/index.ts"; +import { ViewBuilder } from "./ViewBuilder.class.ts"; export interface BuildServerOptions { hotReload?: boolean; @@ -32,7 +32,6 @@ export function buildServer( options: BuildServerOptions ) { const { - hotReload = true, runFromPayload = true, projectRootDir, componentsDir, @@ -40,7 +39,6 @@ export function buildServer( } = options; const viewBuilder = new ViewBuilder({ - autoReload: hotReload, projectRootDir, componentsDir }); @@ -72,8 +70,8 @@ export function buildServer( } export { WebSocketServerInstanciator } from "./websocket/index.ts"; -export { logger } from "./logger.ts"; -export * as config from "./config.ts"; + +export { getApiRouter } from "./endpoints/index.ts"; export { cache