Skip to content

Commit e057fbc

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

File tree

4 files changed

+245
-28
lines changed

4 files changed

+245
-28
lines changed

esbuild.dev.config.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Import Node.js Dependencies
2+
import fs from "node:fs";
3+
import fsAsync from "node:fs/promises";
4+
import http from "node:http";
5+
import path from "node:path";
6+
import consumers from "node:stream/consumers";
7+
import { fileURLToPath } from "node:url";
8+
9+
// Import Third-party Dependencies
10+
import { getBuildConfiguration } from "@nodesecure/documentation-ui/node";
11+
import * as i18n from "@nodesecure/i18n";
12+
import { report } from "@nodesecure/report";
13+
import type { RC } from "@nodesecure/rc";
14+
import esbuild from "esbuild";
15+
import router from "find-my-way";
16+
import sirv from "sirv";
17+
import zup from "zup";
18+
19+
// Import Internal Dependencies
20+
import * as bundle from "./workspaces/server/src/endpoints/bundle.ts";
21+
import * as config from "./workspaces/server/src/endpoints/config.ts";
22+
import * as flags from "./workspaces/server/src/endpoints/flags.ts";
23+
import * as npmDownloads from "./workspaces/server/src/endpoints/npm-downloads.ts";
24+
import * as scorecard from "./workspaces/server/src/endpoints/ossf-scorecard.ts";
25+
import * as search from "./workspaces/server/src/endpoints/search.ts";
26+
import { send } from "./workspaces/server/src/endpoints/util/send.ts";
27+
28+
import english from "./i18n/english.js";
29+
import french from "./i18n/french.js";
30+
31+
// CONSTANTS
32+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
33+
34+
const kPublicDir = path.join(__dirname, "public");
35+
const kOutDir = path.join(__dirname, "dist");
36+
const kImagesDir = path.join(kPublicDir, "img");
37+
const kNodeModulesDir = path.join(__dirname, "node_modules");
38+
const kViewsDir = path.join(__dirname, "views");
39+
const kComponentsDir = path.join(kPublicDir, "components");
40+
const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json");
41+
42+
const kDevPort = Number(process.env.DEV_PORT ?? 8080);
43+
44+
await i18n.getLocalLang();
45+
await i18n.extendFromSystemPath(path.join(__dirname, "i18n"));
46+
47+
const imagesFiles = await fsAsync.readdir(kImagesDir);
48+
49+
await Promise.all([
50+
...imagesFiles
51+
.map((name) => fsAsync.copyFile(path.join(kImagesDir, name), path.join(kOutDir, name))),
52+
fsAsync.copyFile(path.join(kPublicDir, "favicon.ico"), path.join(kOutDir, "favicon.ico"))
53+
]);
54+
55+
const buildContext = await esbuild.context({
56+
entryPoints: [
57+
path.join(kPublicDir, "main.js"),
58+
path.join(kPublicDir, "main.css"),
59+
path.join(kNodeModulesDir, "highlight.js", "styles", "github.css"),
60+
...getBuildConfiguration().entryPoints
61+
],
62+
63+
loader: {
64+
".jpg": "file",
65+
".png": "file",
66+
".woff": "file",
67+
".woff2": "file",
68+
".eot": "file",
69+
".ttf": "file",
70+
".svg": "file"
71+
},
72+
platform: "browser",
73+
bundle: true,
74+
sourcemap: true,
75+
treeShaking: true,
76+
outdir: kOutDir
77+
});
78+
79+
await buildContext.watch();
80+
81+
// Route handlers ALS-dependent & implemented directly
82+
async function serveRoot(_req: http.IncomingMessage, res: http.ServerResponse) {
83+
try {
84+
let html = await fsAsync.readFile(path.join(kViewsDir, "index.html"), "utf-8");
85+
86+
const componentFiles = await fsAsync.readdir(kComponentsDir, { recursive: true });
87+
const htmlComponents = await Promise.all(
88+
(componentFiles as string[])
89+
.filter((f) => f.endsWith(".html"))
90+
.map((f) => fsAsync.readFile(path.join(kComponentsDir, f), "utf-8"))
91+
);
92+
html += htmlComponents.join("");
93+
94+
const i18nLangName = await i18n.getLocalLang();
95+
const rendered = zup(html)({
96+
lang: i18n.getTokenSync("lang"),
97+
i18nLangName,
98+
token: (tokenName: string) => i18n.getTokenSync(`ui.${tokenName}`)
99+
});
100+
101+
res.writeHead(200, { "Content-Type": "text/html" });
102+
res.end(rendered);
103+
}
104+
catch (err: any) {
105+
res.writeHead(500);
106+
res.end(err.message);
107+
}
108+
}
109+
110+
function getI18n(_req: http.IncomingMessage, res: http.ServerResponse) {
111+
send(res, {
112+
english: english.ui,
113+
french: french.ui
114+
});
115+
}
116+
117+
async function getData(_req: http.IncomingMessage, res: http.ServerResponse) {
118+
try {
119+
const payload = JSON.parse(await fsAsync.readFile(kDefaultPayloadPath, "utf-8"));
120+
send(res, payload);
121+
}
122+
catch {
123+
res.statusCode = 204;
124+
res.end();
125+
}
126+
}
127+
128+
// from workspaces/server/src/endpoints/report.ts
129+
// Reads nsecure-result.json from disk instead of ALS context + cache
130+
const kReportPayload: Partial<RC["report"]> = {
131+
includeTransitiveInternal: false,
132+
reporters: ["pdf"],
133+
charts: [
134+
{ name: "Extensions", display: true, interpolation: "d3.interpolateRainbow", type: "bar" },
135+
{ name: "Licenses", display: true, interpolation: "d3.interpolateCool", type: "bar" },
136+
{ name: "Warnings", display: true, type: "horizontalBar", interpolation: "d3.interpolateInferno" },
137+
{ name: "Flags", display: true, type: "horizontalBar", interpolation: "d3.interpolateSinebow" }
138+
]
139+
};
140+
141+
async function postReport(req: http.IncomingMessage, res: http.ServerResponse) {
142+
const { title, includesAllDeps, theme } = await consumers.json(req) as {
143+
title: string;
144+
includesAllDeps: boolean;
145+
theme: "light" | "dark";
146+
};
147+
148+
let scannerPayload: any;
149+
try {
150+
scannerPayload = JSON.parse(fs.readFileSync(kDefaultPayloadPath, "utf-8"));
151+
}
152+
catch {
153+
return send(res, void 0, { code: 500 });
154+
}
155+
156+
const name = scannerPayload.rootDependency.name;
157+
const [organizationPrefixOrRepo, repo] = name.split("/");
158+
const reportPayload = structuredClone({
159+
...kReportPayload,
160+
title,
161+
npm: {
162+
organizationPrefix: repo === undefined ? null : organizationPrefixOrRepo,
163+
packages: [repo === undefined ? organizationPrefixOrRepo : repo]
164+
},
165+
theme
166+
});
167+
168+
try {
169+
const dependencies = includesAllDeps ?
170+
scannerPayload.dependencies :
171+
{ [name]: scannerPayload.dependencies[name] };
172+
173+
const data = await report(dependencies, reportPayload);
174+
175+
return send(res, { data }, { headers: { "content-type": "application/pdf" } });
176+
}
177+
catch (err) {
178+
console.error(err);
179+
180+
return send(res, void 0, { code: 500 });
181+
}
182+
}
183+
184+
const serving = sirv(kOutDir, { dev: true });
185+
186+
// Same as workspaces/server/src/endpoints/index.ts
187+
const apiRouter = router({
188+
ignoreTrailingSlash: true,
189+
defaultRoute: (req, res) => serving(req, res, () => {
190+
res.writeHead(404);
191+
res.end("Not Found");
192+
})
193+
});
194+
195+
apiRouter.get("/", serveRoot);
196+
apiRouter.get("/data", getData);
197+
apiRouter.get("/config", config.get);
198+
apiRouter.put("/config", config.save);
199+
apiRouter.get("/i18n", getI18n);
200+
apiRouter.get("/search/:packageName", search.get);
201+
apiRouter.get("/search-versions/:packageName", search.versions);
202+
apiRouter.get("/flags", flags.getAll);
203+
apiRouter.get("/flags/description/:title", flags.get);
204+
apiRouter.get("/bundle/:packageName", bundle.get);
205+
apiRouter.get("/bundle/:packageName/:version", bundle.get);
206+
apiRouter.get("/downloads/:packageName", npmDownloads.get);
207+
apiRouter.get("/scorecard/:org/:packageName", scorecard.get);
208+
apiRouter.post("/report", postReport);
209+
210+
http.createServer((req, res) => apiRouter.lookup(req, res))
211+
.listen(kDevPort, () => console.log(`Dev server: http://localhost:${kDevPort}`));
212+
213+
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:front:dev && npm run build:workspaces",
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: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -51,37 +51,36 @@ export async function start(
5151
cache.prefix = crypto.randomBytes(4).toString("hex");
5252
}
5353

54-
const httpServer = buildServer(dataFilePath, {
55-
port: httpPort,
56-
hotReload: enableDeveloperMode,
57-
runFromPayload,
58-
projectRootDir: kProjectRootDir,
59-
componentsDir: kComponentsDir,
60-
i18n: {
61-
english,
62-
french
63-
}
64-
});
65-
66-
httpServer.listen(httpPort, async() => {
67-
const link = `http://localhost:${httpServer.address().port}`;
68-
console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link));
69-
70-
open(link);
71-
});
54+
if (enableDeveloperMode) {
55+
// todo: ping/warn if dev server is not up & running
56+
open("http://127.0.0.1:8080");
57+
}
58+
else {
59+
const httpServer = buildServer(dataFilePath, {
60+
port: httpPort,
61+
hotReload: enableDeveloperMode,
62+
runFromPayload,
63+
projectRootDir: kProjectRootDir,
64+
componentsDir: kComponentsDir,
65+
i18n: {
66+
english,
67+
french
68+
}
69+
});
7270

73-
new WebSocketServerInstanciator({
74-
cache,
75-
logger
76-
});
71+
new WebSocketServerInstanciator({
72+
cache,
73+
logger
74+
});
7775

78-
for (const eventName of ["SIGINT", "SIGTERM"]) {
79-
process.on(eventName, () => {
80-
httpServer.close();
76+
for (const eventName of ["SIGINT", "SIGTERM"]) {
77+
process.on(eventName, () => {
78+
httpServer.close();
8179

82-
console.log(kleur.red().bold(`${eventName} signal received.`));
83-
process.exit(0);
84-
});
80+
console.log(kleur.red().bold(`${eventName} signal received.`));
81+
process.exit(0);
82+
});
83+
}
8584
}
8685
}
8786

0 commit comments

Comments
 (0)