Skip to content

Commit df46649

Browse files
author
Raulo Erwan.
committed
tech: enable watch mode & esbuild server in dev mode
1 parent 3ec1738 commit df46649

8 files changed

Lines changed: 175 additions & 33 deletions

File tree

bin/index.js

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

esbuild.dev.config.ts

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

src/commands/http.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
// Import Node.js Dependencies
2+
import crypto from "node:crypto";
23
import fs from "node:fs";
34
import path from "node:path";
4-
import crypto from "node:crypto";
55

66
// Import Third-party Dependencies
7-
import open from "open";
8-
import * as SemVer from "semver";
97
import * as i18n from "@nodesecure/i18n";
108
import {
9+
buildServer,
1110
cache,
1211
logger,
13-
buildServer,
1412
WebSocketServerInstanciator
1513
} from "@nodesecure/server";
14+
import open from "open";
15+
import * as SemVer from "semver";
1616

1717
// Import Internal Dependencies
1818
import english from "../../i18n/english.js";
@@ -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: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// Import Node.js Dependencies
2-
import path from "node:path";
32
import fs from "node:fs/promises";
3+
import path from "node:path";
44

55
// Import Third-party Dependencies
6-
import zup from "zup";
76
import * as i18n from "@nodesecure/i18n";
8-
import chokidar from "chokidar";
97
import { globStream } from "glob";
8+
import zup from "zup";
109

1110
// Import Internal Dependencies
1211
import { logger } from "./logger.ts";
@@ -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: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// Import Node.js Dependencies
22
import fs from "node:fs";
3-
import path from "node:path";
43
import http from "node:http";
4+
import path from "node:path";
55

66
// Import Third-party Dependencies
77
import sirv from "sirv";
88

99
// Import Internal Dependencies
10-
import { getApiRouter } from "./endpoints/index.ts";
11-
import { ViewBuilder } from "./ViewBuilder.class.ts";
1210
import {
1311
context,
1412
type AsyncStoreContext,
1513
type NestedStringRecord
1614
} from "./ALS.ts";
1715
import { cache } from "./cache.ts";
16+
import { getApiRouter } from "./endpoints/index.ts";
17+
import { ViewBuilder } from "./ViewBuilder.class.ts";
1818

1919
export interface BuildServerOptions {
2020
hotReload?: boolean;
@@ -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)