Skip to content

Commit

Permalink
feat: implement HTML template components
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Jan 28, 2025
1 parent 8ffc961 commit 3390a44
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 136 deletions.
2 changes: 2 additions & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,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)
.action(async(spec, options) => {
checkNodeSecureToken();
await commands.scanner.auto(spec, options);
Expand All @@ -71,6 +72,7 @@ prog
.describe(i18n.getTokenSync("cli.commands.open.desc"))
.option("-p, --port", i18n.getTokenSync("cli.commands.open.option_port"), process.env.PORT)
.option("-f, --fresh-start", i18n.getTokenSync("cli.commands.open.option_fresh_start"), process.env.PORT)
.option("-d, --developer", i18n.getTokenSync("cli.commands.open.option_developer"), false)
.action(commands.http.start);

prog
Expand Down
3 changes: 2 additions & 1 deletion i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const cli = {
open: {
desc: "Run an HTTP Server with a given nsecure JSON file",
option_port: "Define the running port",
option_fresh_start: "Launch the server from scratch, ignoring any existing payload file"
option_fresh_start: "Launch the server from scratch, ignoring any existing payload file",
option_developer: "Launch the server in developer mode, enabling automatic HTML component refresh"
},
verify: {
desc: "Run a complete advanced analysis for a given npm package",
Expand Down
3 changes: 2 additions & 1 deletion i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const cli = {
open: {
desc: "Démarre un serveur HTTP avec un fichier .json nsecure donné",
option_port: "Port à utiliser",
option_fresh_start: "Lance le serveur à partir de zéro, en ignorant tout fichier de payload existant"
option_fresh_start: "Lance le serveur à partir de zéro, en ignorant tout fichier de payload existant",
option_developer: "Lance le serveur en mode développeur, permettant le rafraîchissement automatique des composants HTML"
},
verify: {
desc: "Démarre une analyse AST avancée pour un package npm donné",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"@topcli/prompts": "^2.0.0",
"@topcli/spinner": "^2.1.2",
"cacache": "^19.0.1",
"chokidar": "^4.0.3",
"dotenv": "^16.4.5",
"filenamify": "^6.0.0",
"highlightjs-line-numbers.js": "^2.8.0",
Expand Down
98 changes: 98 additions & 0 deletions public/components/searchbar/searchbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template id="search_helpers_default">
<div class="line" data-value="version:"><b>version:</b>
<p>semver</p>
</div>
<div class="line" data-value="package:"><b>package:</b>
<p>name</p>
</div>
<div class="line" data-value="license:"><b>license:</b>
<p>name</p>
</div>
<div class="line" data-value="ext:"><b>ext:</b>
<p>file extension</p>
</div>
<div class="line" data-value="builtin:"><b>builtin:</b>
<p>node.js module</p>
</div>
<div class="line" data-value="author:"><b>author:</b>
<p>name/email</p>
</div>
<div class="line" data-value="flag:"><b>flag:</b>
<p>name</p>
</div>
<div class="line" data-value="size:"><b>size:</b>
<p>size</p>
</div>
</template>

<template id="search_helpers_flags">
<div class="line" data-value="hasExternalCapacity">
<p>🌍 hasExternalCapacity</p>
</div>
<div class="line" data-value="hasIndirectDependencies">
<p>🌲 hasIndirectDependencies</p>
</div>
<div class="line" data-value="hasWarnings">
<p>⚠️ hasWarnings</p>
</div>
<div class="line" data-value="hasNoLicense">
<p>📜 hasNoLicense</p>
</div>
<div class="line" data-value="hasMultipleLicenses">
<p>📚 hasMultipleLicenses</p>
</div>
<div class="line" data-value="hasMinifiedCode">
<p>🔬 hasMinifiedCode</p>
</div>
<div class="line" data-value="hasCustomResolver">
<p>💎 hasCustomResolver</p>
</div>
<div class="line" data-value="hasMissingOrUnusedDependency">
<p>👀 hasMissingOrUnusedDependency</p>
</div>
<div class="line" data-value="hasScript">
<p>📦 hasScript</p>
</div>
<div class="line" data-value="hasNativeCode">
<p>🐲 hasNativeCode</p>
</div>
<div class="line" data-value="hasBannedFile">
<p>⚔️ hasBannedFile</p>
</div>
<div class="line" data-value="isGit">
<p>☁️ isGit</p>
</div>
<div class="line" data-value="isDeprecated">
<p>⛔️ isDeprecated</p>
</div>
<div class="line" data-value="isOutdated">
<p>⌚️ isOutdated</p>
</div>
<div class="line" data-value="hasManyPublishers">
<p>👥 hasManyPublishers</p>
</div>
<div class="line" data-value="isDead">
<p>💀 isDead</p>
</div>
<div class="line" data-value="hasVulnerabilities">
<p>🚨 hasVulnerabilities</p>
</div>
<div class="line" data-value="hasDuplicate">
<p>🎭 hasDuplicate</p>
</div>
</template>

<template id="searchbar-content">
<div id="searchbar">
<i class="icon-globe-alt-outline"></i>
<div class="search-items"></div>
<input type="text" placeholder="[[=z.token('searchbar_placeholder')]]" id="search-bar-input" autocomplete="off"
autocorrect="off" autocapitalize="off" spellcheck="false"></input>

<div class="search-result-background">
<div class="search-result-pannel" id="package-list">
<div class="helpers"></div>
</div>
</div>
</div>
</template>
3 changes: 3 additions & 0 deletions src/commands/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export async function start(
) {
const port = Number(options.port);
const freshStart = Boolean(options.f);
const enableDeveloperMode = Boolean(options.developer);

const fileExtension = path.extname(payloadFileBasename);
if (fileExtension !== ".json" && fileExtension !== "") {
throw new Error("You must provide a JSON file (scanner payload) to open");
Expand All @@ -41,6 +43,7 @@ export async function start(

const httpServer = buildServer(dataFilePath, {
port: Number.isNaN(port) ? 0 : port,
hotReload: enableDeveloperMode,
runFromPayload
});

Expand Down
6 changes: 5 additions & 1 deletion src/commands/scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export async function auto(spec, options) {
);
try {
if (payloadFile !== null) {
await http.start();
const developer = Boolean(commandOptions.developer);

await http.start(void 0, {
developer
});
await events.once(process, "SIGINT");
}
}
Expand Down
96 changes: 96 additions & 0 deletions src/http-server/ViewBuilder.class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Import Node.js Dependencies
import path from "node:path";
import fs from "node:fs/promises";
import { fileURLToPath } from "node:url";

// Import Third-party Dependencies
import zup from "zup";
import * as i18n from "@nodesecure/i18n";
import chokidar from "chokidar";

// Import Internal Dependencies
import { logger } from "./logger.js";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const kProjectRootDir = path.join(__dirname, "..", "..");
const kComponentsDir = path.join(kProjectRootDir, "public", "components");

export class ViewBuilder {
#cached = null;

constructor(options = {}) {
const { autoReload = false } = options;

if (autoReload) {
this.#enableWatcher();
}
}

async #enableWatcher() {
logger.info(`[ViewBuilder] autoReload is enabled`);

const watcher = chokidar.watch(kComponentsDir, {
persistent: false,
awaitWriteFinish: true,
ignored: (path, stats) => stats?.isFile() && !path.endsWith(".html")
});
watcher.on("change", (filePath) => this.#freeCache(filePath));
}

async #freeCache(
filePath
) {
logger.info(`[ViewBuilder] the cache has been released`);
logger.info(`[ViewBuilder](filePath: ${filePath})`);

this.#cached = null;
}

async #build() {
if (this.#cached) {
return this.#cached;
}

let HTMLStr = await fs.readFile(
path.join(kProjectRootDir, "views", "index.html"),
"utf-8"
);

const componentsPromises = [];
for await (
const htmlComponentPath of fs.glob("**/*.html", { cwd: kComponentsDir })
) {
componentsPromises.push(
fs.readFile(
path.join(kComponentsDir, htmlComponentPath),
"utf-8"
)
);
}
const components = await Promise.all(
componentsPromises
);
HTMLStr += components.reduce((prev, curr) => prev + curr, "");

this.#cached = HTMLStr;
logger.info(`[ViewBuilder] the cache has been hydrated`);

return HTMLStr;
}

/**
* @returns {Promise<string>}
*/
async render() {
const i18nLangName = await i18n.getLocalLang();

const HTMLStr = await this.#build();
const templateStr = zup(HTMLStr)({
lang: i18n.getTokenSync("lang"),
i18nLangName,
token: (tokenName) => i18n.getTokenSync(`ui.${tokenName}`)
});

return templateStr;
}
}
28 changes: 5 additions & 23 deletions src/http-server/endpoints/root.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,18 @@
// Import Node.js Dependencies
import { join, dirname } from "node:path";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";

// Import Third-party Dependencies
import zup from "zup";
import send from "@polka/send-type";
import * as i18n from "@nodesecure/i18n";

const __dirname = dirname(fileURLToPath(import.meta.url));
const kProjectRootDir = join(__dirname, "..", "..", "..");

export async function buildHtml() {
const i18nLangName = await i18n.getLocalLang();

const HTMLStr = await readFile(join(kProjectRootDir, "views", "index.html"), "utf-8");
const templateStr = zup(HTMLStr)({
lang: i18n.getTokenSync("lang"),
i18nLangName,
token: (tokenName) => i18n.getTokenSync(`ui.${tokenName}`)
});

return templateStr;
}
// Import Internal Dependencies
import { context } from "../context.js";

export async function get(_req, res) {
try {
res.writeHead(200, {
"Content-Type": "text/html"
});

const templateStr = await buildHtml();
const { viewBuilder } = context.getStore();

const templateStr = await viewBuilder.render();

res.end(templateStr);
}
Expand Down
5 changes: 4 additions & 1 deletion src/http-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ import { appCache } from "./cache.js";
export function buildServer(dataFilePath, options = {}) {
const httpConfigPort = typeof options.port === "number" ? options.port : 0;
const openLink = typeof options.openLink === "boolean" ? options.openLink : true;
const hotReload = typeof options.hotReload === "boolean" ? options.hotReload : true;
const enableWS = options.enableWS ?? process.env.NODE_ENV !== "test";
const runFromPayload = options.runFromPayload ?? true;

const httpServer = polka();

if (runFromPayload) {
fs.accessSync(dataFilePath, fs.constants.R_OK | fs.constants.W_OK);
httpServer.use(middleware.buildContextMiddleware(dataFilePath));
httpServer.use(
middleware.buildContextMiddleware(dataFilePath, hotReload)
);
}
else {
appCache.startFromZero = true;
Expand Down
12 changes: 10 additions & 2 deletions src/http-server/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ import sirv from "sirv";

// Import Internal Dependencies
import { context } from "./context.js";
import { ViewBuilder } from "./ViewBuilder.class.js";

export function buildContextMiddleware(
dataFilePath,
autoReload = false
) {
const viewBuilder = new ViewBuilder({
autoReload
});

export function buildContextMiddleware(dataFilePath) {
return function addContext(_req, _res, next) {
const store = { dataFilePath };
const store = { dataFilePath, viewBuilder };
context.run(store, next);
};
}
Expand Down
11 changes: 3 additions & 8 deletions test/httpServer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import assert from "node:assert";

// Import Third-party Dependencies
import { get, post, MockAgent, getGlobalDispatcher, setGlobalDispatcher } from "@myunisoft/httpie";
import zup from "zup";
import * as i18n from "@nodesecure/i18n";
import * as flags from "@nodesecure/flags";
import enableDestroy from "server-destroy";
Expand All @@ -17,6 +16,7 @@ import cacache from "cacache";

// Require Internal Dependencies
import { buildServer } from "../src/http-server/index.js";
import { ViewBuilder } from "../src/http-server/ViewBuilder.class.js";
import { CACHE_PATH } from "../src/http-server/cache.js";

// CONSTANTS
Expand All @@ -25,7 +25,6 @@ const HTTP_URL = new URL(`http://localhost:${HTTP_PORT}`);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const JSON_PATH = path.join(__dirname, "fixtures", "httpServer.json");
const INDEX_HTML = fs.readFileSync(path.join(__dirname, "..", "views", "index.html"), "utf-8");

const kConfigKey = "___config";
const kGlobalDispatcher = getGlobalDispatcher();
Expand Down Expand Up @@ -65,17 +64,13 @@ describe("httpServer", { concurrency: 1 }, () => {
});

test("'/' should return index.html content", async() => {
const i18nLangName = await i18n.getLocalLang();
const result = await get(HTTP_URL);

assert.equal(result.statusCode, 200);
assert.equal(result.headers["content-type"], "text/html");

const templateStr = zup(INDEX_HTML)({
lang: i18n.getTokenSync("lang"),
i18nLangName,
token: (tokenName) => i18n.getTokenSync(`ui.${tokenName}`)
});
const templateStr = await new ViewBuilder({ autoReload: false }).render();

assert.equal(result.data, templateStr);
});

Expand Down
Loading

0 comments on commit 3390a44

Please sign in to comment.