Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ defaultScannerCommand("from <spec>")
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);
Expand Down
2 changes: 1 addition & 1 deletion docs/cli/auto.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ $ nsecure auto --keep
| `--output` | `-o` | `nsecure-result` | Specify the output file for the results. |
| `--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)). |
| `--keep` | `-k` | `false` | Preserve JSON payload after execution. |
| `--developer` | `-d` | `false` | Launch the server in developer mode, enabling automatic HTML component refresh. |
| `--developer` | | `false` | Launch the server in developer mode, enabling automatic refresh on HTML/CSS/JS changes. |
| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
140 changes: 140 additions & 0 deletions esbuild.dev.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Import Node.js Dependencies
import fsAsync from "node:fs/promises";
import http from "node:http";
import path from "node:path";

// 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 als, type AsyncStoreContext } 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 kPublicDir = path.join(import.meta.dirname, "public");
const kOutDir = path.join(import.meta.dirname, "dist");
const kImagesDir = path.join(kPublicDir, "img");
const kNodeModulesDir = path.join(import.meta.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 Promise.all([
i18n.getLocalLang(),
i18n.extendFromSystemPath(path.join(import.meta.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 = await fsAsync.access(kDefaultPayloadPath).then(() => kDefaultPayloadPath, () => undefined);

if (dataFilePath === undefined) {
cache.startFromZero = true;
}

const store: AsyncStoreContext = {
i18n: { english: { ui: english.ui }, french: { ui: french.ui } },
viewBuilder: new ViewBuilder({
projectRootDir: import.meta.dirname,
componentsDir: kComponentsDir
}),
dataFilePath
};
const htmlWatcher = chokidar.watch(kComponentsDir, {
persistent: false,
awaitWriteFinish: true,
ignored: (path, stats) => (stats?.isFile() ?? false) && !path.endsWith(".html")
});

htmlWatcher.on("change", async(filePath) => {
await buildContext.rebuild().catch(console.error);
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);

const httpServer = http.createServer((req, res) => als.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...");
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions public/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,6 @@ function onSettingsSaved(defaultConfig = null) {
networkView.classList.remove("locked");
});
}

new EventSource("/esbuild").addEventListener("change", () => location.reload());

10 changes: 8 additions & 2 deletions src/commands/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import open from "open";
import * as SemVer from "semver";
import * as i18n from "@nodesecure/i18n";
import {
buildServer,
cache,
logger,
buildServer,
WebSocketServerInstanciator
} from "@nodesecure/server";

Expand Down Expand Up @@ -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,
Expand Down
19 changes: 1 addition & 18 deletions workspaces/server/src/ViewBuilder.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import fs from "node:fs/promises";
// Import Third-party Dependencies
import zup from "zup";
import * as i18n from "@nodesecure/i18n";
import chokidar from "chokidar";
import { globStream } from "glob";

// Import Internal Dependencies
Expand All @@ -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");
Expand Down
10 changes: 8 additions & 2 deletions workspaces/server/src/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Import Node.js Dependencies
import http from "node:http";

// Import Third-party Dependencies
import router from "find-my-way";

Expand All @@ -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);
Expand Down
3 changes: 1 addition & 2 deletions workspaces/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ export function buildServer(
options: BuildServerOptions
) {
const {
hotReload = true,
runFromPayload = true,
projectRootDir,
componentsDir,
i18n
} = options;

const viewBuilder = new ViewBuilder({
autoReload: hotReload,
projectRootDir,
componentsDir
});
Expand Down Expand Up @@ -74,6 +72,7 @@ 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
Expand Down
Loading