From 28ff50dc4d36643c60ad2a7d4a83fda0bbf35148 Mon Sep 17 00:00:00 2001 From: Pierre Demailly Date: Wed, 18 Jun 2025 22:44:45 +0200 Subject: [PATCH] refactor: split codebase into workspaces --- .github/workflows/cache.yml | 37 ++++ .github/workflows/server.yml | 37 ++++ .github/workflows/size-satisfies.yml | 3 +- README.md | 6 +- package.json | 4 +- src/commands/cache.js | 4 +- src/commands/http.js | 31 ++- src/commands/scanner.js | 2 +- src/http-server/index.js | 88 -------- src/http-server/middlewares/static.js | 14 -- test/commands/cache.test.js | 2 +- workspaces/cache/README.md | 126 +++++++++++ workspaces/cache/package.json | 17 ++ src/cache.js => workspaces/cache/src/index.js | 2 +- .../cache/test/index.test.js | 10 +- workspaces/server/README.md | 208 ++++++++++++++++++ workspaces/server/index.js | 75 +++++++ workspaces/server/package.json | 18 ++ {src => workspaces/server/src}/ALS.js | 0 .../server/src}/ViewBuilder.class.js | 28 ++- .../server/src}/config.js | 4 +- .../server/src}/endpoints/bundle.js | 0 .../server/src}/endpoints/config.js | 0 .../server/src}/endpoints/data.js | 4 +- .../server/src}/endpoints/flags.js | 0 .../server/src}/endpoints/i18n.js | 4 +- .../server/src}/endpoints/npm-downloads.js | 0 .../server/src}/endpoints/ossf-scorecard.js | 0 .../server/src}/endpoints/report.js | 2 +- .../server/src}/endpoints/root.js | 3 +- .../server/src}/endpoints/search.js | 2 +- {src => workspaces/server/src}/logger.js | 0 .../server/src}/middlewares/bodyParser.js | 0 .../server/src}/middlewares/context.js | 18 +- .../server/src}/middlewares/index.js | 0 workspaces/server/src/middlewares/static.js | 18 ++ .../server/src}/websocket/commands/remove.js | 0 .../server/src}/websocket/commands/search.js | 0 .../server/src}/websocket/index.js | 4 +- .../server/test/bodyParser.test.js | 2 +- .../server/test}/config.test.js | 6 +- .../server/test}/fixtures/httpServer.json | 0 .../server/test}/httpServer.test.js | 31 +-- 43 files changed, 638 insertions(+), 172 deletions(-) create mode 100644 .github/workflows/cache.yml create mode 100644 .github/workflows/server.yml delete mode 100644 src/http-server/index.js delete mode 100644 src/http-server/middlewares/static.js create mode 100644 workspaces/cache/README.md create mode 100644 workspaces/cache/package.json rename src/cache.js => workspaces/cache/src/index.js (99%) rename test/cache.test.js => workspaces/cache/test/index.test.js (97%) create mode 100644 workspaces/server/README.md create mode 100644 workspaces/server/index.js create mode 100644 workspaces/server/package.json rename {src => workspaces/server/src}/ALS.js (100%) rename {src/http-server => workspaces/server/src}/ViewBuilder.class.js (77%) rename {src/http-server => workspaces/server/src}/config.js (95%) rename {src/http-server => workspaces/server/src}/endpoints/bundle.js (100%) rename {src/http-server => workspaces/server/src}/endpoints/config.js (100%) rename {src/http-server => workspaces/server/src}/endpoints/data.js (94%) rename {src/http-server => workspaces/server/src}/endpoints/flags.js (100%) rename {src/http-server => workspaces/server/src}/endpoints/i18n.js (70%) rename {src/http-server => workspaces/server/src}/endpoints/npm-downloads.js (100%) rename {src/http-server => workspaces/server/src}/endpoints/ossf-scorecard.js (100%) rename {src/http-server => workspaces/server/src}/endpoints/report.js (98%) rename {src/http-server => workspaces/server/src}/endpoints/root.js (91%) rename {src/http-server => workspaces/server/src}/endpoints/search.js (96%) rename {src => workspaces/server/src}/logger.js (100%) rename {src/http-server => workspaces/server/src}/middlewares/bodyParser.js (100%) rename {src/http-server => workspaces/server/src}/middlewares/context.js (51%) rename {src/http-server => workspaces/server/src}/middlewares/index.js (100%) create mode 100644 workspaces/server/src/middlewares/static.js rename {src/http-server => workspaces/server/src}/websocket/commands/remove.js (100%) rename {src/http-server => workspaces/server/src}/websocket/commands/search.js (100%) rename {src/http-server => workspaces/server/src}/websocket/index.js (96%) rename test/bodyPaser.test.js => workspaces/server/test/bodyParser.test.js (90%) rename {test => workspaces/server/test}/config.test.js (91%) rename {test => workspaces/server/test}/fixtures/httpServer.json (100%) rename {test => workspaces/server/test}/httpServer.test.js (93%) diff --git a/.github/workflows/cache.yml b/.github/workflows/cache.yml new file mode 100644 index 00000000..1476c60a --- /dev/null +++ b/.github/workflows/cache.yml @@ -0,0 +1,37 @@ +name: cache CI + +on: + push: + branches: + - master + paths: + - workspaces/cache/** + pull_request: + paths: + - workspaces/cache/** + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x, 22.x] + fail-fast: false + steps: + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run tests + run: npm run test diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml new file mode 100644 index 00000000..334c7268 --- /dev/null +++ b/.github/workflows/server.yml @@ -0,0 +1,37 @@ +name: server CI + +on: + push: + branches: + - master + paths: + - workspaces/server/** + pull_request: + paths: + - workspaces/server/** + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x, 22.x] + fail-fast: false + steps: + - name: Harden Runner + uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 + with: + egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run tests + run: npm run test diff --git a/.github/workflows/size-satisfies.yml b/.github/workflows/size-satisfies.yml index a522b2cd..77122bd6 100644 --- a/.github/workflows/size-satisfies.yml +++ b/.github/workflows/size-satisfies.yml @@ -2,7 +2,8 @@ name: size-satisfies CI on: push: - branches: master + branches: + - master paths: - workspaces/size-satisfies/** pull_request: diff --git a/README.md b/README.md index 26769278..438312de 100644 --- a/README.md +++ b/README.md @@ -211,8 +211,10 @@ Click on one of the links to access the documentation of the workspace: | name | package and link | | --- | --- | | documentation-ui | [@nodesecure/documentation-ui](./workspaces/documentation-ui) | -| vis-network | [@nodesecure/vis-network ](./workspaces/vis-network) | -| size-satisfies | [@nodesecure/size-satisfies ](./workspaces/size-satisfies) | +| vis-network | [@nodesecure/vis-network](./workspaces/vis-network) | +| size-satisfies | [@nodesecure/size-satisfies](./workspaces/size-satisfies) | +| server | [@nodesecure/server](./workspaces/server) | +| cache | [@nodesecure/cache](./workspaces/cache) | These packages are available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). ```bash diff --git a/package.json b/package.json index ad6ba14c..cf29cc48 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "workspaces": [ "workspaces/documentation-ui", "workspaces/vis-network", - "workspaces/size-satisfies" + "workspaces/size-satisfies", + "workspaces/server", + "workspaces/cache" ], "repository": { "type": "git", diff --git a/src/commands/cache.js b/src/commands/cache.js index 481ee8cd..4c1a1080 100644 --- a/src/commands/cache.js +++ b/src/commands/cache.js @@ -5,9 +5,7 @@ import { setImmediate } from "node:timers/promises"; // Import Third-party Dependencies import prettyJson from "@topcli/pretty-json"; import * as i18n from "@nodesecure/i18n"; - -// Import Internal Dependencies -import { appCache } from "../cache.js"; +import { appCache } from "@nodesecure/cache"; export async function main(options) { const { diff --git a/src/commands/http.js b/src/commands/http.js index 5df99bf1..4e074ee9 100644 --- a/src/commands/http.js +++ b/src/commands/http.js @@ -4,16 +4,17 @@ import path from "node:path"; import crypto from "node:crypto"; // Import Third-party Dependencies -import * as SemVer from "semver"; import kleur from "kleur"; +import open from "open"; +import * as SemVer from "semver"; import * as i18n from "@nodesecure/i18n"; - -// Import Internal Dependencies -import { buildServer } from "../http-server/index.js"; -import { appCache } from "../cache.js"; +import { buildServer, WebSocketServerInstanciator } from "@nodesecure/server"; +import { appCache } from "@nodesecure/cache"; // CONSTANTS const kRequiredScannerRange = ">=5.1.0"; +const kProjectRootDir = path.join(import.meta.dirname, "..", ".."); +const kComponentsDir = path.join(kProjectRootDir, "public", "components"); export async function start( payloadFileBasename = "nsecure-result.json", @@ -44,11 +45,27 @@ export async function start( const httpServer = buildServer(dataFilePath, { port: Number.isNaN(port) ? 0 : port, hotReload: enableDeveloperMode, - runFromPayload + runFromPayload, + projectRootDir: kProjectRootDir, + componentsDir: kComponentsDir + }); + + httpServer.listen(port, async() => { + const link = `http://localhost:${port}`; + console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link)); + + open(link); }); + new WebSocketServerInstanciator(); + for (const eventName of ["SIGINT", "SIGTERM"]) { - process.on(eventName, () => httpServer.server.close()); + process.on(eventName, () => { + httpServer.server.close(); + + console.log(kleur.red().bold(`${eventName} signal received.`)); + process.exit(0); + }); } } diff --git a/src/commands/scanner.js b/src/commands/scanner.js index 0a80a501..0f2cfbca 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -10,10 +10,10 @@ import { Spinner } from "@topcli/spinner"; import ms from "ms"; import * as i18n from "@nodesecure/i18n"; import * as Scanner from "@nodesecure/scanner"; +import { appCache } from "@nodesecure/cache"; // Import Internal Dependencies import * as http from "./http.js"; -import { appCache } from "../cache.js"; import { parseContacts } from "./parsers/contacts.js"; export async function auto(spec, options) { diff --git a/src/http-server/index.js b/src/http-server/index.js deleted file mode 100644 index 2b907890..00000000 --- a/src/http-server/index.js +++ /dev/null @@ -1,88 +0,0 @@ -// Import Node.js Dependencies -import fs from "node:fs"; - -// Import Third-party Dependencies -import kleur from "kleur"; -import polka from "polka"; -import open from "open"; -import * as i18n from "@nodesecure/i18n"; - -// Import Internal Dependencies -import * as root from "./endpoints/root.js"; -import * as data from "./endpoints/data.js"; -import * as flags from "./endpoints/flags.js"; -import * as config from "./endpoints/config.js"; -import * as search from "./endpoints/search.js"; -import * as bundle from "./endpoints/bundle.js"; -import * as npmDownloads from "./endpoints/npm-downloads.js"; -import * as scorecard from "./endpoints/ossf-scorecard.js"; -import * as locali18n from "./endpoints/i18n.js"; -import * as report from "./endpoints/report.js"; -import * as middlewares from "./middlewares/index.js"; -import { appCache } from "../cache.js"; -import { WebSocketServerInstanciator } from "./websocket/index.js"; - -// CONSTANTS -export const BROWSER = { - open -}; - -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(); - - const asyncStoreProperties = {}; - if (runFromPayload) { - fs.accessSync(dataFilePath, fs.constants.R_OK | fs.constants.W_OK); - asyncStoreProperties.dataFilePath = dataFilePath; - } - else { - appCache.startFromZero = true; - } - httpServer.use( - middlewares.buildContextMiddleware(hotReload, asyncStoreProperties) - ); - - httpServer.use(middlewares.addStaticFiles); - httpServer.get("/", root.get); - - httpServer.get("/data", data.get); - httpServer.get("/config", config.get); - httpServer.put("/config", config.save); - httpServer.get("/i18n", locali18n.get); - - httpServer.get("/search/:packageName", search.get); - httpServer.get("/search-versions/:packageName", search.versions); - - httpServer.get("/flags", flags.getAll); - httpServer.get("/flags/description/:title", flags.get); - httpServer.get("/bundle/:pkgName", bundle.get); - httpServer.get("/bundle/:pkgName/:version", bundle.get); - httpServer.get("/downloads/:pkgName", npmDownloads.get); - httpServer.get("/scorecard/:org/:pkgName", scorecard.get); - httpServer.post("/report", report.post); - - httpServer.listen(httpConfigPort, async() => { - const port = httpServer.server.address().port; - const link = `http://localhost:${port}`; - console.log(kleur.magenta().bold(await i18n.getToken("cli.http_server_started")), kleur.cyan().bold(link)); - - if (openLink) { - BROWSER.open(link); - } - }); - - enableWS && new WebSocketServerInstanciator(); - - return httpServer; -} - -process.on("SIGINT", () => { - console.log(kleur.red().bold("SIGINT signal received.")); - process.exit(0); -}); diff --git a/src/http-server/middlewares/static.js b/src/http-server/middlewares/static.js deleted file mode 100644 index 8b77d2cc..00000000 --- a/src/http-server/middlewares/static.js +++ /dev/null @@ -1,14 +0,0 @@ -// Import Node.js Dependencies -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -// Import Third-party Dependencies -import sirv from "sirv"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const kProjectRootDir = path.join(__dirname, "..", "..", ".."); - -export const addStaticFiles = sirv( - path.join(kProjectRootDir, "dist"), - { dev: true } -); diff --git a/test/commands/cache.test.js b/test/commands/cache.test.js index 21dc83c0..cb6a53bb 100644 --- a/test/commands/cache.test.js +++ b/test/commands/cache.test.js @@ -11,10 +11,10 @@ import { after, before, describe, it } from "node:test"; // Import Third-party Dependencies import * as i18n from "@nodesecure/i18n"; +import { appCache, DEFAULT_PAYLOAD_PATH } from "@nodesecure/cache"; // Import Internal Dependencies import { arrayFromAsync } from "../helpers/utils.js"; -import { appCache, DEFAULT_PAYLOAD_PATH } from "../../src/cache.js"; import { main } from "../../src/commands/cache.js"; // CONSTANTS diff --git a/workspaces/cache/README.md b/workspaces/cache/README.md new file mode 100644 index 00000000..300583be --- /dev/null +++ b/workspaces/cache/README.md @@ -0,0 +1,126 @@ +# `cache` + +[![version](https://img.shields.io/github/package-json/v/NodeSecure/Cli?filename=workspaces%cache%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/cache) +[![OpenSSF +Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli) +[![mit](https://img.shields.io/github/license/NodeSecure/Cli?style=for-the-badge)](https://github.com/NodeSecure/cli/blob/master/LICENSE) +![size](https://img.shields.io/github/languages/code-size/NodeSecure/cache?style=for-the-badge) +[![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/cli/cache.yml?style=for-the-badge)](https://github.com/NodeSecure/cli/actions?query=workflow%3A%cache+CI%22) + +Caching layer for NodeSecure CLI and server, handling configuration, analysis payloads, and cache state management. + +## Requirements + +- [Node.js](https://nodejs.org/en/) v20 or higher + +## Getting Started + +This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). + +```bash +$ npm i @nodesecure/cache +# or +$ yarn add @nodesecure/cache +``` + +## Features + +- Stores and retrieves configuration and analysis payloads. +- Manages a Most Recently Used (MRU) and Least Recently Used (LRU) list for payloads. +- Supports cache initialization, reset, and removal of old payloads. +- Handles payloads for multiple packages, including local and remote analysis results. + +## Usage example + +```js +import { appCache } from "@nodesecure/cache" + +await appCache.initPayloadsList(); +await appCache.setRootPayload(payload); +``` + +## API + +### `updateConfig(config)` + +Stores a new configuration object in the cache. + +### `getConfig()` + +Retrieves the current configuration object from the cache. + +### `updatePayload(pkg, payload)` + +Saves an analysis payload for a given package. + +**Parameters**: +- `pkg` (`string`): Package name (e.g., `"@nodesecure/scanner@6.0.0"`). +- `payload` (`object`): The analysis result to store. + +> [!NOTE] +> Payloads are stored in the user's home directory under `~/.nsecure/payloads/` + +### `getPayload(pkg)` + +Loads an analysis payload for a given package. + +**Parameters**: +`pkg` (`string`): Package name. + +### `availablePayloads()` + +Lists all available payloads (package names) in the cache. + +### `getPayloadOrNull(pkg)` + +Loads an analysis payload for a given package, or returns `null` if not found. + +**Parameters**: + +- `pkg` (`string`): Package name. + +Returns `null` if not found. + +### `updatePayloadsList(payloadsList)` + +Updates the internal MRU/LRU and available payloads list. + +**Parameters**: + +- `payloadsList` (`object`): The new payloads list object. + +### `payloadsList()` + +Retrieves the current MRU/LRU and available payloads list. + +### `initPayloadsList(options = {})` + +Initializes the payloads list, optionally resetting the cache. + +**Parameters**: + +- `options` (`object`, *optional*): + - `logging` (`boolean`, default: `true`): Enable logging. + - `reset` (`boolean`, default: `false`): If `true`, reset the cache before initializing. + +### `removePayload(pkg)` + +Removes a payload for a given package from the cache. + +**Parameters**: +- `pkg` (`string`): Package name. + +### `removeLastMRU()` + +Removes the least recently used payload if the MRU exceeds the maximum allowed. + +### `setRootPayload(payload, options)` + +Sets a new root payload, updates MRU/LRU, and manages cache state. + +**Parameters**: + +- `payload` (`object`): The analysis result to set as root. +- `options` (`object`): + - `logging` (`boolean`, default: `true`): Enable logging. + - `local` (`boolean`, default: `false`): Mark the payload as local. diff --git a/workspaces/cache/package.json b/workspaces/cache/package.json new file mode 100644 index 00000000..998d7d82 --- /dev/null +++ b/workspaces/cache/package.json @@ -0,0 +1,17 @@ +{ + "name": "@nodesecure/cache", + "version": "1.0.0", + "description": "NodeSecure cache module", + "type": "module", + "main": "./src/index.js", + "files": [ + "src" + ], + "scripts": { + "lint": "eslint src test", + "test": "node --test", + "test:c8": "c8 npm run test" + }, + "author": "GENTILHOMME Thomas ", + "license": "MIT" +} diff --git a/src/cache.js b/workspaces/cache/src/index.js similarity index 99% rename from src/cache.js rename to workspaces/cache/src/index.js index adb5c019..aba19c67 100644 --- a/src/cache.js +++ b/workspaces/cache/src/index.js @@ -7,7 +7,7 @@ import fs from "node:fs"; import cacache from "cacache"; // Import Internal Dependencies -import { logger } from "./logger.js"; +import { logger } from "@nodesecure/server"; // CONSTANTS const kConfigCache = "___config"; diff --git a/test/cache.test.js b/workspaces/cache/test/index.test.js similarity index 97% rename from test/cache.test.js rename to workspaces/cache/test/index.test.js index 69c9b48b..86e05cee 100644 --- a/test/cache.test.js +++ b/workspaces/cache/test/index.test.js @@ -9,22 +9,20 @@ import os from "node:os"; import cacache from "cacache"; // Import Internal Dependencies -import { appCache } from "../src/cache.js"; -import * as config from "../src/http-server/config.js"; +import { appCache } from "../src/index.js"; // CONSTANTS const kPayloadsPath = path.join(os.homedir(), ".nsecure", "payloads"); describe("appCache", () => { - let actualConfig; - before(async() => { appCache.prefix = "test_runner"; - actualConfig = await config.get(); }); after(async() => { - await config.set(actualConfig); + appCache.availablePayloads().forEach((pkg) => { + appCache.removePayload(pkg); + }); }); it("should update and get config", async() => { diff --git a/workspaces/server/README.md b/workspaces/server/README.md new file mode 100644 index 00000000..8dd8b79f --- /dev/null +++ b/workspaces/server/README.md @@ -0,0 +1,208 @@ +# `server` + +[![version](https://img.shields.io/github/package-json/v/NodeSecure/Cli?filename=workspaces%2Fserver%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/server) +[![OpenSSF +Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/cli) +[![mit](https://img.shields.io/github/license/NodeSecure/Cli?style=for-the-badge)](https://github.com/NodeSecure/cli/blob/master/LICENSE) +![size](https://img.shields.io/github/languages/code-size/NodeSecure/server?style=for-the-badge) +[![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/cli/server.yml?style=for-the-badge)](https://github.com/NodeSecure/cli/actions?query=workflow%3A%22server+CI%22) + +NodeSecure CLI's http server based on `polka`. + +## Requirements + +- [Node.js](https://nodejs.org/en/) v20 or higher + +## Getting Started + +This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). + +```bash +$ npm i @nodesecure/server +# or +$ yarn add @nodesecure/server +``` + +## Usage example + +```js +import { buildServer } from "@nodesecure/server"; + +const kProjectRootDir = path.join(import.meta.dirname, "..", ".."); +const kComponentsDir = path.join(kProjectRootDir, "public", "components"); +const kDataFilePath = path.join( + process.cwd(), + "nsecure-result.json" +); + +const httpServer = buildServer(kDataFilePath, { + port: 3000, + projectRootDir: kProjectRootDir, + componentsDir: kComponentsDir +}); + +httpServer.listen(port, async() => { + console.log(`Server listening on port ${port}`); +}); +``` + +## API + +### `buildServer(dataFilePath, options): polka` + +Creates and configures a Polka HTTP server instance for the NodeSecure platform. + +**Parameters** +- `dataFilePath` (`string`): +Path to the data file used by the server. Required if `runFromPayload` is `true`. + +- `options` (`object`): +Configuration options for the server. + - `hotReload` (`boolean`, default: `true`): +Enable or disable hot reloading of server data. + - `runFromPayload` (`boolean`, default: `true`): +If true, the server will use the provided dataFilePath for reading and writing data. If false, the server will start with an empty cache. + - `projectRootDir` (`string`): +The root directory of the project, used for serving static files and resolving paths. + - `componentsDir` (`string`): +Directory containing UI components. + +**Returns** +- `httpServer` (`object`): +A configured **Polka** server instance with all routes and middlewares registered. + +## API Endpoints +The server exposes the following REST API endpoints: + +- `GET /` +Render and return the main HTML page for the NodeSecure UI. + +- `GET /data` +Returns the current analysis payload from the cache. + +- **204**: No content if running from an empty cache. +- **200**: JSON payload with analysis data. + +- `GET /config` +Fetch the current server configuration. + +- `PUT /config` +Update and save the server configuration. + +**Body**: JSON configuration object. + +- `GET /i18n` +Returns UI translations for supported languages (English and French). + +- `GET /search/:packageName` +Search for npm packages by name. + +**Params**: +- `packageName`: The name (or partial name) of the npm package to search for. + +**Response**: +- `count`: Number of results. +- `result`: Array of package objects (name, version, description). + +- `GET /search-versions/:packageName` +Get all available versions for a given npm package. + +**Params**: +- `packageName`: The npm package name. + +**Response**: +Array of version strings. + +- `GET /flags` +List all available NodeSecure flags and their metadata. + +- `GET /flags/description/:title` +Get the HTML description for a specific flag. + +**Params**: +- `title`: The flag name. + +- `GET /bundle/:pkgName` +Get bundle size information for a package from Bundlephobia. + +**Params**: +- `pkgName`: The npm package name. + +- `GET /bundle/:pkgName/:version` +Get bundle size information for a specific version of a package from Bundlephobia. + +**Params**: +- `pkgName`: The npm package name. +- `version`: The package version. + +- `GET /downloads/:pkgName` +Get npm download statistics for the last week for a package. + +**Params**: +- `pkgName`: The npm package name. + +- `GET /scorecard/:org/:pkgName` +Get OSSF Scorecard results for a package repository. + +**Params**: +- `org`: The organization or user. +- `pkgName`: The repository name. + +**Query**: +`platform` (*optional*): The platform (default: `github.com`). + +- `POST /report` +Generate a PDF report for the current analysis. + +**Body**: +- `title`: Report title. +- `includesAllDeps`: Boolean, include all dependencies or only the root. +- `theme`: Report theme. + +**Response**: +PDF file as binary data. + +### Static Files + +All static files (UI, assets, etc.) are served from the project root directory. + +> [!NOTE] +> For more details on each endpoint, see the corresponding files in /src/endpoints. + +## Websocket actions + +The `WebSocketServerInstanciator` class sets up and manages a WebSocket server for real-time communication with NodeSecure clients. It provides live updates and cache management features for package analysis. + +```js +new WebSocketServerInstanciator(); +``` +- Initializes a WebSocket server on port 1338. +- Listens for client connections and incoming messages. + +- `SEARCH`: + +**Request**: +```json +{ + "action": "SEARCH", + "pkg": "" +} +``` + +**Response**: + +Streams scan progress, payload data, and cache state updates. + +- `REMOVE`: + +**Request**: +```json +{ + "action": "REMOVE", + "pkg": "" +} +``` + +**Response**: + +Streams cache state updates after removal. diff --git a/workspaces/server/index.js b/workspaces/server/index.js new file mode 100644 index 00000000..261c213b --- /dev/null +++ b/workspaces/server/index.js @@ -0,0 +1,75 @@ +// Import Node.js Dependencies +import fs from "node:fs"; + +// Import Third-party Dependencies +import polka from "polka"; +import { appCache } from "@nodesecure/cache"; + +// Import Internal Dependencies +import * as root from "./src/endpoints/root.js"; +import * as data from "./src/endpoints/data.js"; +import * as flags from "./src/endpoints/flags.js"; +import * as config from "./src/endpoints/config.js"; +import * as search from "./src/endpoints/search.js"; +import * as bundle from "./src/endpoints/bundle.js"; +import * as npmDownloads from "./src/endpoints/npm-downloads.js"; +import * as scorecard from "./src/endpoints/ossf-scorecard.js"; +import * as locali18n from "./src/endpoints/i18n.js"; +import * as report from "./src/endpoints/report.js"; +import * as middlewares from "./src/middlewares/index.js"; +import { WebSocketServerInstanciator } from "./src/websocket/index.js"; +import { logger } from "./src/logger.js"; + +export function buildServer(dataFilePath, options) { + const { + hotReload = true, + runFromPayload = true, + projectRootDir, + componentsDir + } = options; + + const httpServer = polka(); + + const asyncStoreProperties = {}; + if (runFromPayload) { + fs.accessSync(dataFilePath, fs.constants.R_OK | fs.constants.W_OK); + asyncStoreProperties.dataFilePath = dataFilePath; + } + else { + appCache.startFromZero = true; + } + httpServer.use( + middlewares.buildContextMiddleware({ + hotReload, + storeProperties: asyncStoreProperties, + projectRootDir, + componentsDir + }) + ); + + httpServer.use(middlewares.addStaticFiles({ projectRootDir })); + httpServer.get("/", root.get); + + httpServer.get("/data", data.get); + httpServer.get("/config", config.get); + httpServer.put("/config", config.save); + httpServer.get("/i18n", locali18n.get); + + httpServer.get("/search/:packageName", search.get); + httpServer.get("/search-versions/:packageName", search.versions); + + httpServer.get("/flags", flags.getAll); + httpServer.get("/flags/description/:title", flags.get); + httpServer.get("/bundle/:pkgName", bundle.get); + httpServer.get("/bundle/:pkgName/:version", bundle.get); + httpServer.get("/downloads/:pkgName", npmDownloads.get); + httpServer.get("/scorecard/:org/:pkgName", scorecard.get); + httpServer.post("/report", report.post); + + return httpServer; +} + +export { + WebSocketServerInstanciator, + logger +}; diff --git a/workspaces/server/package.json b/workspaces/server/package.json new file mode 100644 index 00000000..efdd5e11 --- /dev/null +++ b/workspaces/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "@nodesecure/server", + "version": "1.0.0", + "description": "NodeSecure server module", + "type": "module", + "main": "./index.js", + "files": [ + "index.js", + "src" + ], + "scripts": { + "lint": "eslint src test", + "test": "node --test --test-concurrency 1", + "test:c8": "c8 npm run test" + }, + "author": "GENTILHOMME Thomas ", + "license": "MIT" +} diff --git a/src/ALS.js b/workspaces/server/src/ALS.js similarity index 100% rename from src/ALS.js rename to workspaces/server/src/ALS.js diff --git a/src/http-server/ViewBuilder.class.js b/workspaces/server/src/ViewBuilder.class.js similarity index 77% rename from src/http-server/ViewBuilder.class.js rename to workspaces/server/src/ViewBuilder.class.js index ea7216d3..aee57cfb 100644 --- a/src/http-server/ViewBuilder.class.js +++ b/workspaces/server/src/ViewBuilder.class.js @@ -1,7 +1,6 @@ // 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"; @@ -10,17 +9,22 @@ import chokidar from "chokidar"; import { globStream } from "glob"; // 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"); +import { logger } from "./logger.js"; export class ViewBuilder { #cached = null; + projectRootDir = null; + componentsDir = null; + + constructor(options) { + const { + autoReload = false, + projectRootDir, + componentsDir + } = options; - constructor(options = {}) { - const { autoReload = false } = options; + this.projectRootDir = projectRootDir; + this.componentsDir = componentsDir; if (autoReload) { this.#enableWatcher(); @@ -30,7 +34,7 @@ export class ViewBuilder { async #enableWatcher() { logger.info("[ViewBuilder] autoReload is enabled"); - const watcher = chokidar.watch(kComponentsDir, { + const watcher = chokidar.watch(this.componentsDir, { persistent: false, awaitWriteFinish: true, ignored: (path, stats) => stats?.isFile() && !path.endsWith(".html") @@ -53,17 +57,17 @@ export class ViewBuilder { } let HTMLStr = await fs.readFile( - path.join(kProjectRootDir, "views", "index.html"), + path.join(this.projectRootDir, "views", "index.html"), "utf-8" ); const componentsPromises = []; for await ( - const htmlComponentPath of globStream("**/*.html", { cwd: kComponentsDir }) + const htmlComponentPath of globStream("**/*.html", { cwd: this.componentsDir }) ) { componentsPromises.push( fs.readFile( - path.join(kComponentsDir, htmlComponentPath), + path.join(this.componentsDir, htmlComponentPath), "utf-8" ) ); diff --git a/src/http-server/config.js b/workspaces/server/src/config.js similarity index 95% rename from src/http-server/config.js rename to workspaces/server/src/config.js index e9499acf..a96fe8b2 100644 --- a/src/http-server/config.js +++ b/workspaces/server/src/config.js @@ -1,9 +1,9 @@ // Import Third-party Dependencies import { warnings } from "@nodesecure/js-x-ray"; +import { appCache } from "@nodesecure/cache"; // Import Internal Dependencies -import { appCache } from "../cache.js"; -import { logger } from "../logger.js"; +import { logger } from "./logger.js"; const experimentalWarnings = Object.entries(warnings) .flatMap(([warning, { experimental }]) => (experimental ? [warning] : [])); diff --git a/src/http-server/endpoints/bundle.js b/workspaces/server/src/endpoints/bundle.js similarity index 100% rename from src/http-server/endpoints/bundle.js rename to workspaces/server/src/endpoints/bundle.js diff --git a/src/http-server/endpoints/config.js b/workspaces/server/src/endpoints/config.js similarity index 100% rename from src/http-server/endpoints/config.js rename to workspaces/server/src/endpoints/config.js diff --git a/src/http-server/endpoints/data.js b/workspaces/server/src/endpoints/data.js similarity index 94% rename from src/http-server/endpoints/data.js rename to workspaces/server/src/endpoints/data.js index 05a12bc2..25f9d382 100644 --- a/src/http-server/endpoints/data.js +++ b/workspaces/server/src/endpoints/data.js @@ -4,10 +4,10 @@ import path from "node:path"; // Import Third-party Dependencies import send from "@polka/send-type"; +import { appCache } from "@nodesecure/cache"; // Import Internal Dependencies -import { appCache } from "../../cache.js"; -import { logger } from "../../logger.js"; +import { logger } from "../logger.js"; // CONSTANTS const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json"); diff --git a/src/http-server/endpoints/flags.js b/workspaces/server/src/endpoints/flags.js similarity index 100% rename from src/http-server/endpoints/flags.js rename to workspaces/server/src/endpoints/flags.js diff --git a/src/http-server/endpoints/i18n.js b/workspaces/server/src/endpoints/i18n.js similarity index 70% rename from src/http-server/endpoints/i18n.js rename to workspaces/server/src/endpoints/i18n.js index 5bd5d6b7..99e254d6 100644 --- a/src/http-server/endpoints/i18n.js +++ b/workspaces/server/src/endpoints/i18n.js @@ -2,8 +2,8 @@ import send from "@polka/send-type"; // Import Internal Dependencies -import english from "../../../i18n/english.js"; -import french from "../../../i18n/french.js"; +import english from "../../../../i18n/english.js"; +import french from "../../../../i18n/french.js"; export async function get(_req, res) { send( diff --git a/src/http-server/endpoints/npm-downloads.js b/workspaces/server/src/endpoints/npm-downloads.js similarity index 100% rename from src/http-server/endpoints/npm-downloads.js rename to workspaces/server/src/endpoints/npm-downloads.js diff --git a/src/http-server/endpoints/ossf-scorecard.js b/workspaces/server/src/endpoints/ossf-scorecard.js similarity index 100% rename from src/http-server/endpoints/ossf-scorecard.js rename to workspaces/server/src/endpoints/ossf-scorecard.js diff --git a/src/http-server/endpoints/report.js b/workspaces/server/src/endpoints/report.js similarity index 98% rename from src/http-server/endpoints/report.js rename to workspaces/server/src/endpoints/report.js index 58abb5b8..e46d9ac4 100644 --- a/src/http-server/endpoints/report.js +++ b/workspaces/server/src/endpoints/report.js @@ -6,7 +6,7 @@ import { report } from "@nodesecure/report"; import send from "@polka/send-type"; // Import Internal Dependencies -import { context } from "../../ALS.js"; +import { context } from "../ALS.js"; import { bodyParser } from "../middlewares/bodyParser.js"; // TODO: provide a non-file-based API on RC side ? diff --git a/src/http-server/endpoints/root.js b/workspaces/server/src/endpoints/root.js similarity index 91% rename from src/http-server/endpoints/root.js rename to workspaces/server/src/endpoints/root.js index daab7c71..7a09c04b 100644 --- a/src/http-server/endpoints/root.js +++ b/workspaces/server/src/endpoints/root.js @@ -2,7 +2,7 @@ import send from "@polka/send-type"; // Import Internal Dependencies -import { context } from "../../ALS.js"; +import { context } from "../ALS.js"; export async function get(_req, res) { try { @@ -13,7 +13,6 @@ export async function get(_req, res) { const { viewBuilder } = context.getStore(); const templateStr = await viewBuilder.render(); - res.end(templateStr); } catch (err) { diff --git a/src/http-server/endpoints/search.js b/workspaces/server/src/endpoints/search.js similarity index 96% rename from src/http-server/endpoints/search.js rename to workspaces/server/src/endpoints/search.js index 327d9cd1..2f221981 100644 --- a/src/http-server/endpoints/search.js +++ b/workspaces/server/src/endpoints/search.js @@ -3,7 +3,7 @@ import send from "@polka/send-type"; import * as npm from "@nodesecure/npm-registry-sdk"; // Import Internal Dependencies -import { logger } from "../../logger.js"; +import { logger } from "../logger.js"; export async function get(req, res) { const { packageName } = req.params; diff --git a/src/logger.js b/workspaces/server/src/logger.js similarity index 100% rename from src/logger.js rename to workspaces/server/src/logger.js diff --git a/src/http-server/middlewares/bodyParser.js b/workspaces/server/src/middlewares/bodyParser.js similarity index 100% rename from src/http-server/middlewares/bodyParser.js rename to workspaces/server/src/middlewares/bodyParser.js diff --git a/src/http-server/middlewares/context.js b/workspaces/server/src/middlewares/context.js similarity index 51% rename from src/http-server/middlewares/context.js rename to workspaces/server/src/middlewares/context.js index 68b3dca2..398bddbe 100644 --- a/src/http-server/middlewares/context.js +++ b/workspaces/server/src/middlewares/context.js @@ -1,13 +1,19 @@ // Import Internal Dependencies -import { context } from "../../ALS.js"; +import { context } from "../ALS.js"; import { ViewBuilder } from "../ViewBuilder.class.js"; -export function buildContextMiddleware( - autoReload = false, - storeProperties = {} -) { +export function buildContextMiddleware(options) { + const { + autoReload = false, + storeProperties = {}, + projectRootDir, + componentsDir + } = options; + const viewBuilder = new ViewBuilder({ - autoReload + autoReload, + projectRootDir, + componentsDir }); return function addContext(_req, _res, next) { diff --git a/src/http-server/middlewares/index.js b/workspaces/server/src/middlewares/index.js similarity index 100% rename from src/http-server/middlewares/index.js rename to workspaces/server/src/middlewares/index.js diff --git a/workspaces/server/src/middlewares/static.js b/workspaces/server/src/middlewares/static.js new file mode 100644 index 00000000..38ef5061 --- /dev/null +++ b/workspaces/server/src/middlewares/static.js @@ -0,0 +1,18 @@ +// Import Node.js Dependencies +import path from "node:path"; + +// Import Third-party Dependencies +import sirv from "sirv"; + +export function addStaticFiles(options) { + const { + projectRootDir + } = options; + + return sirv( + path.join(projectRootDir, "dist"), + { + dev: true + } + ); +} diff --git a/src/http-server/websocket/commands/remove.js b/workspaces/server/src/websocket/commands/remove.js similarity index 100% rename from src/http-server/websocket/commands/remove.js rename to workspaces/server/src/websocket/commands/remove.js diff --git a/src/http-server/websocket/commands/search.js b/workspaces/server/src/websocket/commands/search.js similarity index 100% rename from src/http-server/websocket/commands/search.js rename to workspaces/server/src/websocket/commands/search.js diff --git a/src/http-server/websocket/index.js b/workspaces/server/src/websocket/index.js similarity index 96% rename from src/http-server/websocket/index.js rename to workspaces/server/src/websocket/index.js index 6437a276..2f00e371 100644 --- a/src/http-server/websocket/index.js +++ b/workspaces/server/src/websocket/index.js @@ -1,10 +1,10 @@ // Import Third-party Dependencies import { WebSocketServer } from "ws"; import { match } from "ts-pattern"; +import { appCache } from "@nodesecure/cache"; // Import Internal Dependencies -import { appCache } from "../../cache.js"; -import { logger } from "../../logger.js"; +import { logger } from "../logger.js"; import { search } from "./commands/search.js"; import { remove } from "./commands/remove.js"; diff --git a/test/bodyPaser.test.js b/workspaces/server/test/bodyParser.test.js similarity index 90% rename from test/bodyPaser.test.js rename to workspaces/server/test/bodyParser.test.js index 6c7db594..b8b3b444 100644 --- a/test/bodyPaser.test.js +++ b/workspaces/server/test/bodyParser.test.js @@ -3,7 +3,7 @@ import { test } from "node:test"; import assert from "node:assert"; // Import Internal Dependencies -import { bodyParser } from "../src/http-server/middlewares/bodyParser.js"; +import { bodyParser } from "../src/middlewares/bodyParser.js"; function generateFakeReq(headers = {}) { return { diff --git a/test/config.test.js b/workspaces/server/test/config.test.js similarity index 91% rename from test/config.test.js rename to workspaces/server/test/config.test.js index 6a777736..03d4d2a8 100644 --- a/test/config.test.js +++ b/workspaces/server/test/config.test.js @@ -5,15 +5,15 @@ import assert from "node:assert"; // Import Third-party Dependencies import cacache from "cacache"; import { warnings } from "@nodesecure/js-x-ray"; +import { CACHE_PATH } from "@nodesecure/cache"; // Import Internal Dependencies -import { get, set } from "../src/http-server/config.js"; -import { CACHE_PATH } from "../src/cache.js"; +import { get, set } from "../src/config.js"; // CONSTANTS const kConfigKey = "___config"; -describe("config", { concurrency: 1 }, () => { +describe("config", () => { let actualConfig; before(async() => { diff --git a/test/fixtures/httpServer.json b/workspaces/server/test/fixtures/httpServer.json similarity index 100% rename from test/fixtures/httpServer.json rename to workspaces/server/test/fixtures/httpServer.json diff --git a/test/httpServer.test.js b/workspaces/server/test/httpServer.test.js similarity index 93% rename from test/httpServer.test.js rename to workspaces/server/test/httpServer.test.js index 42fac9ce..65f7690b 100644 --- a/test/httpServer.test.js +++ b/workspaces/server/test/httpServer.test.js @@ -1,7 +1,7 @@ // Import Node.js Dependencies import fs from "node:fs"; import { fileURLToPath } from "node:url"; -import { after, before, describe, test, mock } from "node:test"; +import { after, before, describe, test } from "node:test"; import { once } from "node:events"; import path from "node:path"; import assert from "node:assert"; @@ -9,16 +9,16 @@ import stream from "node:stream"; // Import Third-party Dependencies import { get, post, MockAgent, getGlobalDispatcher, setGlobalDispatcher } from "@myunisoft/httpie"; +import { CACHE_PATH } from "@nodesecure/cache"; import * as i18n from "@nodesecure/i18n"; import * as flags from "@nodesecure/flags"; import enableDestroy from "server-destroy"; import cacache from "cacache"; // Import Internal Dependencies -import { buildServer, BROWSER } from "../src/http-server/index.js"; -import { CACHE_PATH } from "../src/cache.js"; -import * as rootEndpoint from "../src/http-server/endpoints/root.js"; -import * as flagsEndpoint from "../src/http-server/endpoints/flags.js"; +import { buildServer } from "../index.js"; +import * as rootEndpoint from "../src/endpoints/root.js"; +import * as flagsEndpoint from "../src/endpoints/flags.js"; // CONSTANTS const kHttpPort = 17049; @@ -32,6 +32,8 @@ const kGlobalDispatcher = getGlobalDispatcher(); const kMockAgent = new MockAgent(); const kBundlephobiaPool = kMockAgent.get("https://bundlephobia.com"); const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json"); +const kProjectRootDir = path.join(import.meta.dirname, "..", "..", ".."); +const kComponentsDir = path.join(kProjectRootDir, "public", "components"); describe("httpServer", { concurrency: 1 }, () => { let httpServer; @@ -39,14 +41,17 @@ describe("httpServer", { concurrency: 1 }, () => { before(async() => { setGlobalDispatcher(kMockAgent); await i18n.extendFromSystemPath( - path.join(__dirname, "..", "i18n") + path.join(__dirname, "..", "..", "..", "i18n") ); httpServer = buildServer(JSON_PATH, { port: kHttpPort, openLink: false, - enableWS: false + enableWS: false, + projectRootDir: kProjectRootDir, + componentsDir: kComponentsDir }); + httpServer.listen(kHttpPort); await once(httpServer.server, "listening"); enableDestroy(httpServer.server); @@ -332,25 +337,25 @@ describe("httpServer", { concurrency: 1 }, () => { describe("httpServer without options", () => { let httpServer; - let spawnOpen; // We want to disable WS process.env.NODE_ENV = "test"; before(async() => { - spawnOpen = mock.method(BROWSER, "open", () => void 0); - httpServer = buildServer(JSON_PATH); + httpServer = buildServer(JSON_PATH, { + projectRootDir: kProjectRootDir, + componentsDir: kComponentsDir + }); + httpServer.listen(); await once(httpServer.server, "listening"); enableDestroy(httpServer.server); }); after(async() => { httpServer.server.destroy(); - spawnOpen.mock.restore(); }); - test("should listen on random port and call childProcess.spawn method ('open' pkg) to open link", async() => { + test("should listen on random port", async() => { assert.ok(httpServer.server.address().port > 0); - assert.strictEqual(spawnOpen.mock.callCount(), 1); }); });