diff --git a/README.md b/README.md
index 0671430e..2e6fa89e 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,7 @@ Type a package name directly to search, or prefix with a filter name followed by
- `ext` — file extension present in the package (e.g. `.js`, `.ts`).
- `builtin` — Node.js core module used by the package (e.g. `fs`, `path`).
- `size` — size range (see [size-satisfies](https://github.com/NodeSecure/size-satisfies#usage-example), e.g. `>50kb`, `10kb..200kb`).
+- `highlighted` — all highlighted packages by default.
## FAQ
diff --git a/bin/index.js b/bin/index.js
index 6aa098d2..b082adb6 100755
--- a/bin/index.js
+++ b/bin/index.js
@@ -144,6 +144,7 @@ function defaultScannerCommand(name, options = {}) {
.option("-d, --depth", i18n.getTokenSync("cli.commands.option_depth"), Infinity)
.option("--silent", i18n.getTokenSync("cli.commands.option_silent"), false)
.option("-c, --contacts", i18n.getTokenSync("cli.commands.option_contacts"), [])
+ .option("-p, --packages", i18n.getTokenSync("cli.commands.option_packages"), [])
.option("--verbose", i18n.getTokenSync("cli.commands.option_verbose"), false);
if (includeOutput) {
diff --git a/docs/cli/auto.md b/docs/cli/auto.md
index b1cc61b7..824509a2 100644
--- a/docs/cli/auto.md
+++ b/docs/cli/auto.md
@@ -28,4 +28,6 @@ $ nsecure auto --keep
| `--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` | | `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. |
+| `--contacts` | `-c` | `[]` | List of contacts to highlight. |
+| `--packages` | `-p` | `[]` | List of packages to highlight. |
+`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
diff --git a/docs/cli/cwd.md b/docs/cli/cwd.md
index 5d4888de..9def6099 100644
--- a/docs/cli/cwd.md
+++ b/docs/cli/cwd.md
@@ -18,4 +18,6 @@ $ nsecure cwd [options]
| `--silent` | | `false` | Suppress console output, making execution silent. |
| `--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)). |
-| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
+| `--contacts` | `-c` | `[]` | List of contacts to highlight. |
+| `--packages` | `-p` | `[]` | List of packages to highlight. |
+`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
diff --git a/docs/cli/from.md b/docs/cli/from.md
index 7613cd14..b2676d74 100644
--- a/docs/cli/from.md
+++ b/docs/cli/from.md
@@ -24,4 +24,6 @@ $ nsecure from express@3.0.0 -o express-report
| `--silent` | | `false` | Suppress console output, making execution silent. |
| `--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)). |
-| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
+| `--contacts` | `-c` | `[]` | List of contacts to highlight. |
+| `--packages` | `-p` | `[]` | List of packages to highlight. |
+`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. |
diff --git a/i18n/arabic.js b/i18n/arabic.js
index 18ff9c0d..11619d88 100644
--- a/i18n/arabic.js
+++ b/i18n/arabic.js
@@ -22,6 +22,7 @@ const cli = {
option_output: "اسم ملف JSON الناتج",
option_silent: "تفعيل الوضع الصامت الذي يعطل مؤشرات CLI",
option_contacts: "قائمة جهات الاتصال للتمييز",
+ option_packages: "قائمة الحزم للتمييز",
option_verbose: "ضبط مستوى الـ log الخاص بالـ CLI على verbose، مما يجعل الـ CLI يولّد logs أكثر تفصيلاً.",
strategy: "مصدر الثغرات للاستخدام",
cwd: {
@@ -232,7 +233,8 @@ const ui = {
legend: {
default: "الحزمة بخير.",
warn: "الحزمة بها تحذيرات.",
- friendly: "الحزمة تتم صيانتها بواسطة نفس مؤلفي الحزمة الجذرية."
+ friendly: "الحزمة تتم صيانتها بواسطة نفس مؤلفي الحزمة الجذرية.",
+ highlighted: "الحزمة جزء من الحزم المميزة"
},
lockedNavigation: {
next: "التالي",
diff --git a/i18n/english.js b/i18n/english.js
index 85a4f833..105af881 100644
--- a/i18n/english.js
+++ b/i18n/english.js
@@ -24,6 +24,7 @@ const cli = {
option_output: "Json file output name",
option_silent: "enable silent mode which disable CLI spinners",
option_contacts: "List of contacts to hightlight",
+ option_packages: "List of packages to highlight",
option_verbose: "Sets cli log level to verbose, causing the CLI to output more detailed logs.",
strategy: "Vulnerabilities source to use",
cwd: {
@@ -258,13 +259,15 @@ const ui = {
author: "name or email",
ext: "file extension",
builtin: "node.js module",
- size: "e.g. >50kb"
+ size: "e.g. >50kb",
+ highlighted: "all"
}
},
legend: {
default: "The package is fine.",
warn: "The package has warnings.",
- friendly: "The package is maintained by the same authors as the root package."
+ friendly: "The package is maintained by the same authors as the root package.",
+ highlighted: "The package is part of highlighted packages"
},
lockedNavigation: {
next: "Next",
diff --git a/i18n/french.js b/i18n/french.js
index 419ca2f3..182968cf 100644
--- a/i18n/french.js
+++ b/i18n/french.js
@@ -24,6 +24,7 @@ const cli = {
option_output: "Nom de sortie du fichier json",
option_silent: "Activer le mode silencieux qui désactive les spinners du CLI",
option_contacts: "Liste des contacts à mettre en évidence",
+ option_packages: "Liste des packages à mettre en évidence",
option_verbose: "Définir le niveau de log CLI à verbeux, ce qui amènera la CLI à générer des logs plus détaillés.",
strategy: "Source de vulnérabilités à utiliser",
cwd: {
@@ -258,13 +259,15 @@ const ui = {
author: "nom ou email",
ext: "extension de fichier",
builtin: "module node.js",
- size: "ex. >50kb"
+ size: "ex. >50kb",
+ highlighted: "all"
}
},
legend: {
default: "Rien à signaler.",
warn: "La dépendance contient des menaces.",
- friendly: "La dépendance est maintenu par des auteurs du package principal."
+ friendly: "La dépendance est maintenu par des auteurs du package principal.",
+ highlighted: "Le package fait partie des packages mis en évidence"
},
lockedNavigation: {
next: "Suivant",
diff --git a/i18n/turkish.js b/i18n/turkish.js
index e1c26b80..55dd8c17 100644
--- a/i18n/turkish.js
+++ b/i18n/turkish.js
@@ -24,6 +24,7 @@ const cli = {
option_output: "JSON dosyası çıktı adı",
option_silent: "CLI döndürücülerini devre dışı bırakan sessiz modu etkinleştir",
option_contacts: "Vurgulanacak kişilerin listesi",
+ option_packages: "Vurgulanacak paketlerin listesi",
option_verbose: "CLI'nin log seviyesini verbose olarak ayarlar, bu da CLI'nin daha ayrıntılı loglar üretmesine neden olur.",
strategy: "Kullanılacak güvenlik açığı kaynağı",
cwd: {
@@ -234,7 +235,8 @@ const ui = {
legend: {
default: "Paket sorunsuz.",
warn: "Pakette uyarılar var.",
- friendly: "Paket, kök paketin yazarlarıyla aynı kişiler tarafından bakılmaktadır."
+ friendly: "Paket, kök paketin yazarlarıyla aynı kişiler tarafından bakılmaktadır.",
+ highlighted: "Paket, vurgulanan paketlerin bir parçasıdır"
},
lockedNavigation: {
next: "Sonraki",
diff --git a/public/components/legend/legend.js b/public/components/legend/legend.js
index 46cb9b1d..0a4d28f9 100644
--- a/public/components/legend/legend.js
+++ b/public/components/legend/legend.js
@@ -90,6 +90,7 @@ class Legend extends LitElement {
${this.#createLegendBoxElement(colors.WARN, legend.warn)}
${this.#createLegendBoxElement(colors.FRIENDLY, legend.friendly)}
${this.#createLegendBoxElement(colors.DEFAULT, legend.default)}
+ ${this.#createLegendBoxElement(colors.HIGHLIGHTED, legend.highlighted)}
`;
}
@@ -98,7 +99,7 @@ class Legend extends LitElement {
theme,
text
) {
- const style = `background-color: ${theme.color}; color: ${theme.font.color};`;
+ const style = `background-color: ${theme.color}; color: ${(theme.font ?? COLORS.LIGHT.DEFAULT.font).color};`;
return html`
diff --git a/public/components/search-command/filters.js b/public/components/search-command/filters.js
index 6e05e27f..1080ced4 100644
--- a/public/components/search-command/filters.js
+++ b/public/components/search-command/filters.js
@@ -20,11 +20,13 @@ export const VERSION_PRESETS = [
{ label: "≥ 1.0", value: ">=1.0.0" },
{ label: "< 1.0", value: "<1.0.0" }
];
-export const FILTERS_NAME = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size"]);
+export const FILTERS_NAME = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size", "highlighted"]);
// Filters that use a searchable text-based list (not a rich visual panel)
export const FILTER_HAS_HELPERS = new Set(["license", "ext", "builtin", "author"]);
// Filters where the mode persists after selection (multi-select)
export const FILTER_MULTI_SELECT = new Set(["flag"]);
+// Filters that auto-confirm immediately on selection (no text input needed)
+export const FILTER_INSTANT_CONFIRM = new Set(["highlighted"]);
/**
* Returns per-flag package counts across the full linker.
@@ -236,6 +238,8 @@ function matchesFilter(opt, filterName, inputValue) {
}
case "flag":
return opt.flags.includes(inputValue);
+ case "highlighted":
+ return inputValue === "true" ? opt.isHighlighted === true : opt.isHighlighted !== true;
default:
return false;
}
diff --git a/public/components/search-command/search-command.js b/public/components/search-command/search-command.js
index 3ca97266..201ff79f 100644
--- a/public/components/search-command/search-command.js
+++ b/public/components/search-command/search-command.js
@@ -9,6 +9,7 @@ import {
FILTERS_NAME,
FILTER_HAS_HELPERS,
FILTER_MULTI_SELECT,
+ FILTER_INSTANT_CONFIRM,
computeMatches,
getHelperValues
} from "./filters.js";
@@ -59,12 +60,13 @@ class SearchCommand extends LitElement {
#init = ({ detail: { linker, packages, network } }) => {
this.#linker = linker;
this.#network = network;
- this.#packages = packages.map(({ id, name, version, flags }) => {
+ this.#packages = packages.map(({ id, name, version, flags, isHighlighted }) => {
return {
id: String(id),
name,
version,
- flags
+ flags,
+ isHighlighted
};
});
};
@@ -280,10 +282,15 @@ class SearchCommand extends LitElement {
#selectHelper(helper) {
if (helper.type === "filter") {
- this.inputValue = `${helper.value}:`;
- this.activeFilter = helper.value;
- this.selectedIndex = -1;
- this.results = [];
+ if (FILTER_INSTANT_CONFIRM.has(helper.value)) {
+ this.#addQuery(helper.value, "true");
+ }
+ else {
+ this.inputValue = `${helper.value}:`;
+ this.activeFilter = helper.value;
+ this.selectedIndex = -1;
+ this.results = [];
+ }
}
else {
this.#addQuery(this.activeFilter, helper.value);
diff --git a/public/main.js b/public/main.js
index 64cf442b..8b6b7bef 100644
--- a/public/main.js
+++ b/public/main.js
@@ -247,12 +247,7 @@ function onSettingsSaved(defaultConfig = null) {
window.settings.config.theme = theme;
window.settings.config.disableExternalRequests = config.disableExternalRequests;
- if (theme === "dark") {
- document.body.classList.add("dark");
- }
- else {
- document.body.classList.remove("dark");
- }
+ document.body.classList.toggle("dark", theme === "dark");
await secureDataSet.init(
secureDataSet.data,
diff --git a/src/commands/parsers/packages.js b/src/commands/parsers/packages.js
new file mode 100644
index 00000000..2c04157c
--- /dev/null
+++ b/src/commands/parsers/packages.js
@@ -0,0 +1,70 @@
+/**
+ * Parse a list of CLI package strings into the expected HighlightPackages format expected
+ * by @nodesecure/scanner: `string[] | Record`.
+ *
+ * Each input string can be:
+ * - "lodash" → plain name, no version constraint
+ * - "lodash@^4.0.0" → name with a semver range
+ * - "lodash@1.0.0,2.0.0" → name with a list of specific versions
+ * - "@scope/pkg" → scoped package, no version constraint
+ * - "@scope/pkg@^1.0.0" → scoped package with a semver range
+ *
+ * When none of the entries carry a version constraint the function returns a plain `string[]`.
+ * If at least one entry has a version constraint the function returns a `Record`;
+ * Entries without a constraint are mapped to '*'
+ *
+ * @param {string | string[]} input
+ * @returns {string[] | Record}
+ */
+export function parsePackages(input) {
+ const items = Array.isArray(input) ? input : [input];
+ const parsed = items.map(parsePackage);
+
+ const hasVersionConstraints = parsed.some(({ version }) => version !== null);
+
+ if (!hasVersionConstraints) {
+ return parsed.map(({ name }) => name);
+ }
+
+ return Object.fromEntries(
+ parsed.map(({ name, version }) => [name, version ?? "*"])
+ );
+}
+
+/**
+ * @param {string} str
+ * @returns {{ name: string, version: string | string[] | null }}
+ */
+function parsePackage(str) {
+ // Scoped packages start with "@", so search for a second "@" after index 1.
+ const versionSeparator = str.startsWith("@") ? str.indexOf("@", 1) : str.indexOf("@");
+
+ if (versionSeparator === -1) {
+ return { name: str.trim(), version: null };
+ }
+
+ const name = str.slice(0, versionSeparator).trim();
+ const versionStr = str.slice(versionSeparator + 1).trim();
+
+ if (versionStr === "") {
+ return { name, version: null };
+ }
+
+ if (versionStr.includes(",")) {
+ const versions = versionStr.split(",").map((v) => v.trim()).filter(Boolean);
+ let version;
+ if (versions.length === 0) {
+ version = null;
+ }
+ else if (versions.length === 1) {
+ version = versions[0];
+ }
+ else {
+ version = versions;
+ }
+
+ return { name, version };
+ }
+
+ return { name, version: versionStr };
+}
diff --git a/src/commands/scanner.js b/src/commands/scanner.js
index ba48a2b5..3b83f7c8 100644
--- a/src/commands/scanner.js
+++ b/src/commands/scanner.js
@@ -15,6 +15,7 @@ import * as scanner from "@nodesecure/scanner";
import kleur from "../utils/styleText.js";
import * as http from "./http.js";
import { parseContacts } from "./parsers/contacts.js";
+import { parsePackages } from "./parsers/packages.js";
export async function auto(spec, options) {
const { keep, ...commandOptions } = options;
@@ -22,7 +23,8 @@ export async function auto(spec, options) {
const optionsWithContacts = {
...commandOptions,
highlight: {
- contacts: parseContacts(options.contacts)
+ contacts: parseContacts(options.contacts),
+ packages: parsePackages(options.packages ?? [])
}
};
@@ -67,6 +69,7 @@ export async function cwd(options) {
vulnerabilityStrategy,
silent,
contacts,
+ packages: highlightPackages = [],
verbose
} = options;
@@ -74,7 +77,7 @@ export async function cwd(options) {
process.cwd(),
{
maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy, highlight:
- { contacts: parseContacts(contacts) }, isVerbose: verbose
+ { contacts: parseContacts(contacts), packages: parsePackages(highlightPackages) }, isVerbose: verbose
},
initLogger(void 0, !silent)
);
@@ -83,7 +86,15 @@ export async function cwd(options) {
}
export async function from(spec, options) {
- const { depth: maxDepth = Infinity, output, silent, contacts, vulnerabilityStrategy, verbose } = options;
+ const {
+ depth: maxDepth = Infinity,
+ output,
+ silent,
+ contacts,
+ packages: highlightPackages = [],
+ vulnerabilityStrategy,
+ verbose
+ } = options;
const payload = await scanner.from(
spec,
@@ -91,7 +102,8 @@ export async function from(spec, options) {
maxDepth,
vulnerabilityStrategy,
highlight: {
- contacts: parseContacts(contacts)
+ contacts: parseContacts(contacts),
+ packages: parsePackages(highlightPackages)
},
isVerbose: verbose
},
diff --git a/test/commands/parsers/packages.test.js b/test/commands/parsers/packages.test.js
new file mode 100644
index 00000000..213f1b82
--- /dev/null
+++ b/test/commands/parsers/packages.test.js
@@ -0,0 +1,108 @@
+// Import Node.js Dependencies
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+
+// Import Internal Dependencies
+import { parsePackages } from "../../../src/commands/parsers/packages.js";
+
+describe("packages parser", () => {
+ describe("returns string[] when no version constraints", () => {
+ it("should parse a single plain package name", () => {
+ assert.deepEqual(parsePackages("lodash"), ["lodash"]);
+ });
+
+ it("should parse a single scoped package with no version", () => {
+ assert.deepEqual(parsePackages("@scope/pkg"), ["@scope/pkg"]);
+ });
+
+ it("should parse multiple plain packages with no versions", () => {
+ assert.deepEqual(parsePackages(["lodash", "express"]), ["lodash", "express"]);
+ });
+
+ it("should trim package names", () => {
+ assert.deepEqual(parsePackages(" lodash "), ["lodash"]);
+ });
+ });
+
+ describe("returns Record when at least one entry has a version constraint", () => {
+ it("should parse a package with a semver range", () => {
+ assert.deepEqual(parsePackages("lodash@^4.0.0"), { lodash: "^4.0.0" });
+ });
+
+ it("should parse a package with a list of specific versions", () => {
+ assert.deepEqual(parsePackages("lodash@1.0.0,2.0.0"), { lodash: ["1.0.0", "2.0.0"] });
+ });
+
+ it("should parse a scoped package with a semver range", () => {
+ assert.deepEqual(parsePackages("@scope/pkg@^1.0.0"), { "@scope/pkg": "^1.0.0" });
+ });
+
+ it("should map entries without a version to '*' when mixed with versioned entries", () => {
+ assert.deepEqual(
+ parsePackages(["lodash", "express@^4.0.0"]),
+ { lodash: "*", express: "^4.0.0" }
+ );
+ });
+
+ it("should parse multiple packages all with version constraints", () => {
+ assert.deepEqual(
+ parsePackages(["lodash@^4.0.0", "express@^4.18.0"]),
+ { lodash: "^4.0.0", express: "^4.18.0" }
+ );
+ });
+
+ it("should trim version strings", () => {
+ assert.deepEqual(parsePackages("lodash@ ^4.0.0 "), { lodash: "^4.0.0" });
+ });
+
+ it("should trim individual versions in a comma-separated list", () => {
+ assert.deepEqual(parsePackages("lodash@1.0.0 , 2.0.0"), { lodash: ["1.0.0", "2.0.0"] });
+ });
+ });
+
+ describe("JSON array string input", () => {
+ it("should parse a JSON array string of plain packages", () => {
+ assert.deepEqual(
+ parsePackages('["@hapi/formula@3.0.2", "@hapi/tlds@1.1.6"]'),
+ { "@hapi/formula": "3.0.2", "@hapi/tlds": "1.1.6" }
+ );
+ });
+
+ it("should parse a JSON array string with no version constraints", () => {
+ assert.deepEqual(
+ parsePackages('["lodash", "express"]'),
+ ["lodash", "express"]
+ );
+ });
+
+ it("should fall through to plain string parsing for invalid JSON starting with '['", () => {
+ assert.deepEqual(parsePackages("[notjson"), ["[notjson"]);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should return an empty array for an empty array input", () => {
+ assert.deepEqual(parsePackages([]), []);
+ });
+
+ it("should treat a scoped name with no slash and no version as a plain package name", () => {
+ assert.deepEqual(parsePackages("@scope"), ["@scope"]);
+ });
+
+ it("should treat a trailing '@' with no version as no version constraint", () => {
+ assert.deepEqual(parsePackages("lodash@"), ["lodash"]);
+ });
+
+ it("should treat a scoped package with trailing '@' as no version constraint", () => {
+ assert.deepEqual(parsePackages("@scope/pkg@"), ["@scope/pkg"]);
+ });
+
+ it("should ignore a trailing comma in a version list", () => {
+ assert.deepEqual(parsePackages("lodash@1.0.0,"), { lodash: "1.0.0" });
+ });
+
+ it("should treat a lone comma as version as no version constraint", () => {
+ assert.deepEqual(parsePackages("lodash@,"), ["lodash"]);
+ });
+ });
+});
diff --git a/workspaces/vis-network/src/constants.js b/workspaces/vis-network/src/constants.js
index 4a954927..8aaf56e2 100644
--- a/workspaces/vis-network/src/constants.js
+++ b/workspaces/vis-network/src/constants.js
@@ -10,9 +10,9 @@
export const COLORS = Object.freeze({
LIGHT: {
SELECTED: {
- color: "#4527A0",
+ color: "#BFC5E0",
font: {
- color: "#FFF"
+ color: "#443730"
}
},
SELECTED_GROUP: {
@@ -28,23 +28,26 @@ export const COLORS = Object.freeze({
}
},
DEFAULT: {
- color: "#E3F2FD",
+ color: "#BEE7E8",
font: {
color: "#121533"
}
},
WARN: {
- color: "#EF5350",
+ color: "#FFBFA0",
font: {
- color: "#FFF"
+ color: "#6B2737"
}
},
FRIENDLY: {
- color: "#e3fde3",
+ color: "#EDEEC0",
font: {
color: "#0e4522"
}
},
+ HIGHLIGHTED: {
+ color: "#EA9010"
+ },
CONNECTED_IN: {
color: "#C8E6C9",
font: {
@@ -101,6 +104,9 @@ export const COLORS = Object.freeze({
color: "#FFF"
}
},
+ HIGHLIGHTED: {
+ color: "#dec42c"
+ },
CONNECTED_IN: {
color: "rgb(89, 44, 109)",
font: {
diff --git a/workspaces/vis-network/src/dataset.js b/workspaces/vis-network/src/dataset.js
index 0ecab670..f980ff9d 100644
--- a/workspaces/vis-network/src/dataset.js
+++ b/workspaces/vis-network/src/dataset.js
@@ -16,6 +16,7 @@ export default class NodeSecureDataSet extends EventTarget {
*/
#highligthedContacts;
+ #highlightedPackages;
constructor(options = {}) {
super();
@@ -94,6 +95,8 @@ export default class NodeSecureDataSet extends EventTarget {
return acc;
}, { names: new Set(), emails: new Set() });
+ this.#highlightedPackages = new Set(data.highlighted.packages);
+
const dependencies = Object.entries(data.dependencies);
this.dependenciesCount = dependencies.length;
@@ -124,6 +127,7 @@ export default class NodeSecureDataSet extends EventTarget {
opt.version = currVersion;
opt.hidden = false;
opt.hasWarnings = hasWarnings;
+ opt.isHighlighted = this.#isHighlightedPackage(packageName, currVersion);
this.computeAuthor(author, `${packageName}@${currVersion}`, contributors);
@@ -160,7 +164,8 @@ export default class NodeSecureDataSet extends EventTarget {
hasWarnings,
flags: flagStr.replace(/\s/g, ""),
links,
- isFriendly
+ isFriendly,
+ isHighlighted: opt.isHighlighted
});
const label = `${packageName}@${currVersion}${flagStr}\n[${prettyBytes(size)}]`;
@@ -168,6 +173,7 @@ export default class NodeSecureDataSet extends EventTarget {
id,
hasWarnings,
isFriendly,
+ isHighlighted: opt.isHighlighted,
theme: this.theme.toUpperCase()
});
color.font.multi = "html";
@@ -229,6 +235,10 @@ export default class NodeSecureDataSet extends EventTarget {
return this.#highligthedContacts.names.has(contact.name) || this.#highligthedContacts.emails.has(contact.email);
}
+ #isHighlightedPackage(name, version) {
+ return this.#highlightedPackages.has(name) || this.#highlightedPackages.has(`${name}@${version}`);
+ }
+
findPackagesByName(name) {
return this.packages.filter((pkg) => pkg.name === name);
}
diff --git a/workspaces/vis-network/src/network.js b/workspaces/vis-network/src/network.js
index 78155ffa..b172a5b0 100644
--- a/workspaces/vis-network/src/network.js
+++ b/workspaces/vis-network/src/network.js
@@ -220,6 +220,12 @@ export default class NodeSecureNetwork {
this.colors.HARDTOREAD;
Object.assign(node, color);
+
+ const { isHighlighted } = this.linker.get(Number(node.id));
+ if (isHighlighted) {
+ node.shadow = { enabled: false };
+ node.borderWidth = 1;
+ }
}
for (const nodeId of nodeIdsToHighlight) {
@@ -267,9 +273,9 @@ export default class NodeSecureNetwork {
this.highlightEnabled = false;
for (const node of allNodes) {
- const { id, hasWarnings, isFriendly } = this.linker.get(Number(node.id));
+ const { id, hasWarnings, isFriendly, isHighlighted } = this.linker.get(Number(node.id));
- Object.assign(node, utils.getNodeColor({ id, hasWarnings, theme: this.theme, isFriendly }));
+ Object.assign(node, utils.getNodeColor({ id, hasWarnings, theme: this.theme, isFriendly, isHighlighted }));
}
this.lastHighlightedIds = null;
@@ -317,6 +323,38 @@ export default class NodeSecureNetwork {
}
}
+ /**
+ * Returns the selected color for a node, preserving highlighted border+shadow if applicable.
+ * @param {number} nodeId
+ * @param {"SELECTED" | "SELECTED_LOCK"} colorKey
+ */
+ #selectedColor(nodeId, colorKey) {
+ const { isHighlighted } = this.linker.get(Number(nodeId));
+ const base = this.colors[colorKey];
+
+ if (!isHighlighted) {
+ return base;
+ }
+
+ const borderColor = CONSTANTS.COLORS[this.theme].HIGHLIGHTED.color;
+
+ return {
+ color: {
+ background: base.color,
+ border: borderColor
+ },
+ font: base.font,
+ borderWidth: 2,
+ shadow: {
+ enabled: true,
+ color: borderColor,
+ size: 12,
+ x: 0,
+ y: 0
+ }
+ };
+ }
+
lockedNeighbourHighlight(params) {
if (this.lastHighlightedIds === null) {
return false;
@@ -338,7 +376,7 @@ export default class NodeSecureNetwork {
}
const color = node.id === selectedNode ?
- this.colors.SELECTED_LOCK :
+ this.#selectedColor(node.id, "SELECTED_LOCK") :
this.colors.SELECTED_GROUP;
Object.assign(node, color);
@@ -397,6 +435,12 @@ export default class NodeSecureNetwork {
// mark all nodes as hard to read.
for (const node of Object.values(allNodes)) {
Object.assign(node, this.colors.HARDTOREAD);
+
+ const { isHighlighted } = this.linker.get(Number(node.id));
+ if (isHighlighted) {
+ node.shadow = { enabled: false };
+ node.borderWidth = 1;
+ }
}
// get the second degree nodes
@@ -420,7 +464,7 @@ export default class NodeSecureNetwork {
}
// the main node gets its own color and its label back.
- Object.assign(allNodes[selectedNode], this.colors.SELECTED);
+ Object.assign(allNodes[selectedNode], this.#selectedColor(selectedNode, "SELECTED"));
// select and label edges connected to the selected node
const connectedEdges = this.network.getConnectedEdges(selectedNode);
@@ -445,9 +489,9 @@ export default class NodeSecureNetwork {
else if (this.highlightEnabled) {
this.highlightEnabled = false;
for (const node of Object.values(allNodes)) {
- const { id, hasWarnings, isFriendly } = this.linker.get(Number(node.id));
+ const { id, hasWarnings, isFriendly, isHighlighted } = this.linker.get(Number(node.id));
- Object.assign(node, utils.getNodeColor({ id, hasWarnings, theme: this.theme, isFriendly }));
+ Object.assign(node, utils.getNodeColor({ id, hasWarnings, theme: this.theme, isFriendly, isHighlighted }));
}
}
diff --git a/workspaces/vis-network/src/utils.js b/workspaces/vis-network/src/utils.js
index b385f1a8..d65ab1f1 100644
--- a/workspaces/vis-network/src/utils.js
+++ b/workspaces/vis-network/src/utils.js
@@ -39,27 +39,54 @@ export async function getJSON(path, customHeaders = Object.create(null)) {
* @param {string} options.hasWarnings
* @param {string} options.theme
* @param {string} options.isFriendly
+ * @param {boolean} [options.isHighlighted]
*/
export function getNodeColor(options) {
const {
id,
hasWarnings = false,
theme = "LIGHT",
- isFriendly = false
+ isFriendly = false,
+ isHighlighted = false
} = options;
// id 0 is the root package (so by default he is highlighted as selected).
if (id === 0) {
return CONSTANTS.COLORS[theme].SELECTED;
}
- else if (hasWarnings) {
- return CONSTANTS.COLORS[theme].WARN;
+
+ let nodeColor;
+ if (hasWarnings) {
+ nodeColor = CONSTANTS.COLORS[theme].WARN;
}
else if (isFriendly) {
- return CONSTANTS.COLORS[theme].FRIENDLY;
+ nodeColor = CONSTANTS.COLORS[theme].FRIENDLY;
+ }
+ else {
+ nodeColor = CONSTANTS.COLORS[theme].DEFAULT;
+ }
+
+ if (isHighlighted) {
+ const borderColor = CONSTANTS.COLORS[theme].HIGHLIGHTED_BORDER;
+
+ return {
+ color: {
+ background: nodeColor.color,
+ border: borderColor
+ },
+ font: nodeColor.font,
+ borderWidth: 2,
+ shadow: {
+ enabled: true,
+ color: borderColor,
+ size: 12,
+ x: 0,
+ y: 0
+ }
+ };
}
- return CONSTANTS.COLORS[theme].DEFAULT;
+ return nodeColor;
}
export function getFlagsEmojisInlined(
diff --git a/workspaces/vis-network/test/dataset-payload.json b/workspaces/vis-network/test/dataset-payload.json
index ccd1add6..2601137f 100644
--- a/workspaces/vis-network/test/dataset-payload.json
+++ b/workspaces/vis-network/test/dataset-payload.json
@@ -34,6 +34,10 @@
"string-width"
]
}
+ ],
+ "packages": [
+ "pkg3",
+ "pkg2@1.0.4"
]
},
"dependencies": {
diff --git a/workspaces/vis-network/test/dataset.test.js b/workspaces/vis-network/test/dataset.test.js
index 30d63902..ae55accc 100644
--- a/workspaces/vis-network/test/dataset.test.js
+++ b/workspaces/vis-network/test/dataset.test.js
@@ -70,6 +70,37 @@ test("NodeSecureDataSet.isHighlighted", async() => {
"email: gentilhomme.thomas@gmail.com should be hightlighted");
});
+test("NodeSecureDataSet.init should mark highlighted packages by name", async() => {
+ const nsDataSet = new NodeSecureDataSet();
+ await nsDataSet.init(dataSetPayload);
+
+ const pkg3Packages = nsDataSet.findPackagesByName("pkg3");
+ assert.ok(pkg3Packages.length > 0, "should have pkg3 packages");
+ assert.ok(pkg3Packages.every((pkg) => pkg.isHighlighted), "all pkg3 versions should be highlighted (matched by name)");
+});
+
+test("NodeSecureDataSet.init should mark highlighted packages by name@version", async() => {
+ const nsDataSet = new NodeSecureDataSet();
+ await nsDataSet.init(dataSetPayload);
+
+ const pkg2Packages = nsDataSet.findPackagesByName("pkg2");
+ const highlighted = pkg2Packages.find((pkg) => pkg.version === "1.0.4");
+ const notHighlighted = pkg2Packages.find((pkg) => pkg.version === "1.0.3");
+
+ assert.ok(highlighted, "should find pkg2@1.0.4");
+ assert.equal(highlighted.isHighlighted, true, "pkg2@1.0.4 should be highlighted (matched by name@version)");
+ assert.equal(notHighlighted.isHighlighted, false, "pkg2@1.0.3 should not be highlighted");
+});
+
+test("NodeSecureDataSet.init should not highlight packages absent from highlighted list", async() => {
+ const nsDataSet = new NodeSecureDataSet();
+ await nsDataSet.init(dataSetPayload);
+
+ const pkg1Packages = nsDataSet.findPackagesByName("pkg1");
+ assert.ok(pkg1Packages.length > 0, "should have pkg1 packages");
+ assert.ok(pkg1Packages.every((pkg) => !pkg.isHighlighted), "pkg1 should not be highlighted");
+});
+
test("NodeSecureDataSet.computeAuthors", () => {
const nsDataSet = new NodeSecureDataSet();
nsDataSet.computeAuthor({ name: "John Doe" }, "pkg@1.1");
@@ -118,7 +149,8 @@ test("NodeSecureDataSet.findPackagesByName should have packages when name matche
hasWarnings: false,
flags: "",
links: undefined,
- isFriendly: 0
+ isFriendly: 0,
+ isHighlighted: false
},
{
id: undefined,
@@ -127,7 +159,9 @@ test("NodeSecureDataSet.findPackagesByName should have packages when name matche
hasWarnings: false,
flags: "",
links: undefined,
- isFriendly: 0
+ isFriendly: 0,
+ isHighlighted: true
+
}
];