Skip to content

Commit 40ac566

Browse files
ErwanRauloRaulo Erwan.
andauthored
tech: enable watch mode & esbuild server in dev mode (#674)
Co-authored-by: Raulo Erwan. <erwan.raulo.externe@emeria.eu>
1 parent 5e2d8ca commit 40ac566

9 files changed

Lines changed: 165 additions & 26 deletions

File tree

bin/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ defaultScannerCommand("from <spec>")
6363
defaultScannerCommand("auto [spec]", { includeOutput: false, strategy: vulnera.strategies.GITHUB_ADVISORY })
6464
.describe(i18n.getTokenSync("cli.commands.auto.desc"))
6565
.option("-k, --keep", i18n.getTokenSync("cli.commands.auto.option_keep"), false)
66-
.option("-d, --developer", i18n.getTokenSync("cli.commands.open.option_developer"), false)
66+
.option("--developer", i18n.getTokenSync("cli.commands.open.option_developer"), false)
6767
.action(async(spec, options) => {
6868
checkNodeSecureToken();
6969
await commands.scanner.auto(spec, options);

docs/cli/auto.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ $ nsecure auto --keep
2727
| `--output` | `-o` | `nsecure-result` | Specify the output file for the results. |
2828
| `--vulnerabilityStrategy` | `-s` | github-advisory | Strategy used to fetch package vulnerabilities (see Vulnera [available strategy](https://github.com/NodeSecure/vulnera?tab=readme-ov-file#available-strategy)). |
2929
| `--keep` | `-k` | `false` | Preserve JSON payload after execution. |
30-
| `--developer` | `-d` | `false` | Launch the server in developer mode, enabling automatic HTML component refresh. |
30+
| `--developer` | | `false` | Launch the server in developer mode, enabling automatic refresh on HTML/CSS/JS changes. |
3131
| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |

esbuild.dev.config.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Import Node.js Dependencies
2+
import fsAsync from "node:fs/promises";
3+
import http from "node:http";
4+
import path from "node:path";
5+
6+
// Import Third-party Dependencies
7+
import { getBuildConfiguration } from "@nodesecure/documentation-ui/node";
8+
import * as i18n from "@nodesecure/i18n";
9+
import chokidar from "chokidar";
10+
import esbuild from "esbuild";
11+
import open from "open";
12+
import sirv from "sirv";
13+
14+
// Import Internal Dependencies
15+
import english from "./i18n/english.js";
16+
import french from "./i18n/french.js";
17+
import { context as als, type AsyncStoreContext } from "./workspaces/server/src/ALS.ts";
18+
import { ViewBuilder } from "./workspaces/server/src/ViewBuilder.class.ts";
19+
import { cache } from "./workspaces/server/src/cache.ts";
20+
import { getApiRouter } from "./workspaces/server/src/endpoints/index.ts";
21+
import { logger } from "./workspaces/server/src/logger.ts";
22+
import { WebSocketServerInstanciator } from "./workspaces/server/src/websocket/index.ts";
23+
24+
// CONSTANTS
25+
const kPublicDir = path.join(import.meta.dirname, "public");
26+
const kOutDir = path.join(import.meta.dirname, "dist");
27+
const kImagesDir = path.join(kPublicDir, "img");
28+
const kNodeModulesDir = path.join(import.meta.dirname, "node_modules");
29+
const kComponentsDir = path.join(kPublicDir, "components");
30+
const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json");
31+
const kDevPort = Number(process.env.DEV_PORT ?? 8080);
32+
33+
await Promise.all([
34+
i18n.getLocalLang(),
35+
i18n.extendFromSystemPath(path.join(import.meta.dirname, "i18n"))
36+
]);
37+
38+
const imagesFiles = await fsAsync.readdir(kImagesDir);
39+
40+
await Promise.all([
41+
...imagesFiles
42+
.map((name) => fsAsync.copyFile(path.join(kImagesDir, name), path.join(kOutDir, name))),
43+
fsAsync.copyFile(path.join(kPublicDir, "favicon.ico"), path.join(kOutDir, "favicon.ico"))
44+
]);
45+
46+
const buildContext = await esbuild.context({
47+
entryPoints: [
48+
path.join(kPublicDir, "main.js"),
49+
path.join(kPublicDir, "main.css"),
50+
path.join(kNodeModulesDir, "highlight.js", "styles", "github.css"),
51+
...getBuildConfiguration().entryPoints
52+
],
53+
54+
loader: {
55+
".jpg": "file",
56+
".png": "file",
57+
".woff": "file",
58+
".woff2": "file",
59+
".eot": "file",
60+
".ttf": "file",
61+
".svg": "file"
62+
},
63+
platform: "browser",
64+
bundle: true,
65+
sourcemap: true,
66+
treeShaking: true,
67+
outdir: kOutDir
68+
});
69+
70+
await buildContext.watch();
71+
72+
const { hosts: esbuildHosts, port: esbuildPort } = await buildContext.serve({
73+
servedir: kOutDir
74+
});
75+
76+
const dataFilePath = await fsAsync.access(kDefaultPayloadPath).then(() => kDefaultPayloadPath, () => undefined);
77+
78+
if (dataFilePath === undefined) {
79+
cache.startFromZero = true;
80+
}
81+
82+
const store: AsyncStoreContext = {
83+
i18n: { english: { ui: english.ui }, french: { ui: french.ui } },
84+
viewBuilder: new ViewBuilder({
85+
projectRootDir: import.meta.dirname,
86+
componentsDir: kComponentsDir
87+
}),
88+
dataFilePath
89+
};
90+
const htmlWatcher = chokidar.watch(kComponentsDir, {
91+
persistent: false,
92+
awaitWriteFinish: true,
93+
ignored: (path, stats) => (stats?.isFile() ?? false) && !path.endsWith(".html")
94+
});
95+
96+
htmlWatcher.on("change", async(filePath) => {
97+
await buildContext.rebuild().catch(console.error);
98+
store.viewBuilder.freeCache(filePath);
99+
});
100+
101+
const serving = sirv(kOutDir, { dev: true });
102+
103+
function defaultRoute(req: http.IncomingMessage, res: http.ServerResponse) {
104+
if (req.url === "/esbuild") {
105+
const proxyReq = http.request(
106+
{ hostname: esbuildHosts[0], port: esbuildPort, path: req.url, method: req.method, headers: req.headers },
107+
(proxyRes) => {
108+
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
109+
proxyRes.pipe(res);
110+
}
111+
);
112+
113+
proxyReq.on("error", (err) => {
114+
console.error(`[proxy/esbuild] ${err.message}`);
115+
res.writeHead(502);
116+
res.end("Bad Gateway");
117+
});
118+
119+
req.pipe(proxyReq);
120+
121+
return;
122+
}
123+
124+
serving(req, res, () => {
125+
res.writeHead(404);
126+
res.end("Not Found");
127+
});
128+
}
129+
130+
const apiRouter = getApiRouter(defaultRoute);
131+
132+
const httpServer = http.createServer((req, res) => als.run(store, () => apiRouter.lookup(req, res)))
133+
.listen(kDevPort, () => {
134+
console.log(`Dev server: http://localhost:${kDevPort}`);
135+
open(`http://localhost:${kDevPort}`);
136+
});
137+
138+
new WebSocketServerInstanciator({ cache, logger });
139+
140+
console.log("Watching...");

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"lint-fix": "npm run lint -- --fix",
1818
"prepublishOnly": "rimraf ./dist && npm run build && pkg-ok",
1919
"build": "npm run build:front && npm run build:workspaces",
20+
"build:dev": "npm run build:workspaces && npm run build:front:dev",
2021
"build:front": "node ./esbuild.config.js",
22+
"build:front:dev": "node --experimental-strip-types ./esbuild.dev.config.ts",
2123
"build:workspaces": "npm run build --ws --if-present",
2224
"test": "npm run test:cli && npm run lint && npm run lint:css",
2325
"test:cli": "node --no-warnings --test test/**/*.test.js",

public/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,6 @@ function onSettingsSaved(defaultConfig = null) {
301301
networkView.classList.remove("locked");
302302
});
303303
}
304+
305+
new EventSource("/esbuild").addEventListener("change", () => location.reload());
306+

src/commands/http.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import open from "open";
88
import * as SemVer from "semver";
99
import * as i18n from "@nodesecure/i18n";
1010
import {
11+
buildServer,
1112
cache,
1213
logger,
13-
buildServer,
1414
WebSocketServerInstanciator
1515
} from "@nodesecure/server";
1616

@@ -51,9 +51,15 @@ export async function start(
5151
cache.prefix = crypto.randomBytes(4).toString("hex");
5252
}
5353

54+
if (enableDeveloperMode) {
55+
const link = "http://127.0.0.1:8080";
56+
console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link));
57+
open(link);
58+
59+
return;
60+
}
5461
const httpServer = buildServer(dataFilePath, {
5562
port: httpPort,
56-
hotReload: enableDeveloperMode,
5763
runFromPayload,
5864
projectRootDir: kProjectRootDir,
5965
componentsDir: kComponentsDir,

workspaces/server/src/ViewBuilder.class.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import fs from "node:fs/promises";
55
// Import Third-party Dependencies
66
import zup from "zup";
77
import * as i18n from "@nodesecure/i18n";
8-
import chokidar from "chokidar";
98
import { globStream } from "glob";
109

1110
// Import Internal Dependencies
@@ -24,31 +23,15 @@ export class ViewBuilder {
2423

2524
constructor(options: ViewBuilderOptions) {
2625
const {
27-
autoReload = false,
2826
projectRootDir,
2927
componentsDir
3028
} = options;
3129

3230
this.projectRootDir = projectRootDir;
3331
this.componentsDir = componentsDir;
34-
35-
if (autoReload) {
36-
this.#enableWatcher();
37-
}
38-
}
39-
40-
async #enableWatcher() {
41-
logger.info("[ViewBuilder] autoReload is enabled");
42-
43-
const watcher = chokidar.watch(this.componentsDir, {
44-
persistent: false,
45-
awaitWriteFinish: true,
46-
ignored: (path, stats) => (stats?.isFile() ?? false) && !path.endsWith(".html")
47-
});
48-
watcher.on("change", (filePath) => this.#freeCache(filePath));
4932
}
5033

51-
async #freeCache(
34+
freeCache(
5235
filePath: string
5336
) {
5437
logger.info("[ViewBuilder] the cache has been released");

workspaces/server/src/endpoints/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Import Node.js Dependencies
2+
import http from "node:http";
3+
14
// Import Third-party Dependencies
25
import router from "find-my-way";
36

@@ -13,9 +16,12 @@ import * as scorecard from "./ossf-scorecard.ts";
1316
import * as locali18n from "./i18n.ts";
1417
import * as report from "./report.ts";
1518

16-
export function getApiRouter() {
19+
type DefaultRoute = (req: http.IncomingMessage, res: http.ServerResponse) => void;
20+
21+
export function getApiRouter(defaultRoute?: DefaultRoute) {
1722
const apiRouter = router({
18-
ignoreTrailingSlash: true
23+
ignoreTrailingSlash: true,
24+
defaultRoute
1925
});
2026

2127
apiRouter.get("/", root.get);

workspaces/server/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ export function buildServer(
3232
options: BuildServerOptions
3333
) {
3434
const {
35-
hotReload = true,
3635
runFromPayload = true,
3736
projectRootDir,
3837
componentsDir,
3938
i18n
4039
} = options;
4140

4241
const viewBuilder = new ViewBuilder({
43-
autoReload: hotReload,
4442
projectRootDir,
4543
componentsDir
4644
});
@@ -74,6 +72,7 @@ export function buildServer(
7472
export { WebSocketServerInstanciator } from "./websocket/index.ts";
7573
export { logger } from "./logger.ts";
7674
export * as config from "./config.ts";
75+
export { getApiRouter } from "./endpoints/index.ts";
7776

7877
export {
7978
cache

0 commit comments

Comments
 (0)