chore(cli): replace ejs with native node template rendering (#12109)

* chore(cli): replace ejs with native node template rendering

* chore(cli): add HTML escaping to audit, changelog, and licenses rendering functions

* chore(cli): remove templates directory from dist script in package.json

* chore(cli): remove node-fetch dependency from package.json
This commit is contained in:
Denys Vuika
2026-07-30 13:22:47 +01:00
committed by GitHub
parent 4f32198afd
commit 0c099eafb0
11 changed files with 1217 additions and 1233 deletions
+47 -29
View File
@@ -18,17 +18,56 @@
*/
import { spawnSync } from 'node:child_process';
import * as ejs from 'ejs';
import * as path from 'path';
import * as fs from 'fs';
import { argv, exit } from 'node:process';
import { parseArgs } from 'node:util';
import { escapeHtml } from './utils';
interface AuditCommandArgs {
package?: string;
outDir?: string;
}
function renderAuditPage(jsonAudit: any, projName: string, projVersion: string): string {
const rows: string[] = [];
if (jsonAudit.auditReportVersion >= 2) {
for (const key in jsonAudit.vulnerabilities) {
const v = jsonAudit.vulnerabilities[key];
rows.push(`|${escapeHtml(v.severity)} | ${escapeHtml(v.name)} | ${JSON.stringify(v.range)} |`);
}
} else {
for (const key in jsonAudit.advisories) {
const a = jsonAudit.advisories[key];
rows.push(`|${escapeHtml(a.severity)} | ${escapeHtml(a.module_name)} | ${JSON.stringify(a.vulnerable_versions)} |`);
}
}
return `---
Title: Audit info, ${escapeHtml(projName)} ${escapeHtml(projVersion)}
---
# Audit information for ${escapeHtml(projName)} ${escapeHtml(projVersion)}
This page lists the security audit of the dependencies this project depends on.
## Risks
- Critical risk: ${jsonAudit.metadata.vulnerabilities.critical}
- High risk: ${jsonAudit.metadata.vulnerabilities.high}
- Moderate risk: ${jsonAudit.metadata.vulnerabilities.moderate}
- Low risk: ${jsonAudit.metadata.vulnerabilities.low}
Dependencies analyzed: ${jsonAudit.metadata.totalDependencies}
## Libraries
| Severity | Module | Vulnerable versions |
| --- | --- | --- |
${rows.join('\n')}
`;
}
/**
* Audit report command
*
@@ -82,12 +121,6 @@ Options:
exit(1);
}
const templatePath = path.resolve(__dirname, '../templates/auditPage.ejs');
if (!fs.existsSync(templatePath)) {
console.error(`Cannot find the report template: ${templatePath}`);
exit(1);
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-console
console.log(`Running audit on ${packagePath}`);
@@ -133,29 +166,14 @@ Options:
return;
}
ejs.renderFile(
templatePath,
{
jsonAudit,
projVersion: packageJson.version,
projName: packageJson.name
},
{},
(err: any, mdText: string) => {
if (err) {
console.error(err);
reject(err);
} else {
const outputPath = path.resolve(options.outDir || workingDir);
const outputFile = path.join(outputPath, `audit-info-${packageJson.version}.md`);
const mdText = renderAuditPage(jsonAudit, packageJson.name, packageJson.version);
const outputPath = path.resolve(options.outDir || workingDir);
const outputFile = path.join(outputPath, `audit-info-${packageJson.version}.md`);
fs.writeFileSync(outputFile, mdText);
fs.writeFileSync(outputFile, mdText);
// eslint-disable-next-line no-console
console.log(`Report saved as ${outputFile}`);
resolve(0);
}
}
);
// eslint-disable-next-line no-console
console.log(`Report saved as ${outputFile}`);
resolve(0);
});
}
+49 -35
View File
@@ -25,7 +25,7 @@ import { spawnSync } from 'node:child_process';
import * as path from 'path';
import { logger } from './logger';
import * as fs from 'fs';
import * as ejs from 'ejs';
import { escapeHtml } from './utils';
interface Commit {
hash: string;
@@ -167,6 +167,39 @@ function commitAuthorAllowed(commit: Commit, authorFilter: string): boolean {
return !(filterRegex.test(commit.author) || filterRegex.test(commit.author_email));
}
function renderChangelogMd(commits: Commit[], projName: string, projVersion: string, repoUrl: string): string {
const lines = commits.map((c) => `- [${c.hash}](${repoUrl}/commit/${c.hash}) ${escapeHtml(c.subject)}`);
return `---
Title: Changelog for ${escapeHtml(projName)} v${escapeHtml(projVersion)}
---
# Changelog
${lines.join('\n')}
`;
}
function renderChangelogHtml(commits: Commit[], projName: string, projVersion: string, repoUrl: string): string {
const items = commits
.map((c) => ` <li>\n <a href="${repoUrl}/commit/${c.hash}">[${c.hash}]</a> ${escapeHtml(c.subject)}\n </li>`)
.join('\n');
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog for ${escapeHtml(projName)} v${escapeHtml(projVersion)}</title>
</head>
<body>
<h1>Changelog</h1>
<ul>
${items}
</ul>
</body>
</html>`;
}
/**
* Changelog command
*
@@ -265,42 +298,23 @@ Options:
exit(1);
}
const templatePath = path.resolve(__dirname, `../templates/changelog-${format}.ejs`);
if (!fs.existsSync(templatePath)) {
console.error(`Cannot find the report template: ${templatePath}`);
exit(1);
}
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
const packageJson = JSON.parse(fs.readFileSync(packagePath).toString());
ejs.renderFile(
templatePath,
{
remote,
repo_url,
commits,
projVersion: packageJson.version,
projName: packageJson.name
},
{},
(err: any, text: string) => {
if (err) {
console.error(err);
reject(err);
} else {
if (output) {
const outputDir = path.resolve(output);
const outputFile = path.join(outputDir, `changelog-${packageJson.version}.${format}`);
console.log('Writing changelog to', outputFile);
const text =
format === 'html'
? renderChangelogHtml(commits, packageJson.name, packageJson.version, repo_url)
: renderChangelogMd(commits, packageJson.name, packageJson.version, repo_url);
fs.writeFileSync(outputFile, text);
} else {
console.log(text);
}
resolve(0);
}
}
);
if (output) {
const outputDir = path.resolve(output);
const outputFile = path.join(outputDir, `changelog-${packageJson.version}.${format}`);
console.log('Writing changelog to', outputFile);
fs.writeFileSync(outputFile, text);
} else {
console.log(text);
}
resolve(0);
});
}
+34 -29
View File
@@ -22,7 +22,7 @@ import { parseArgs } from 'node:util';
import * as path from 'path';
import * as fs from 'fs';
import * as licenseList from 'spdx-license-list';
import * as ejs from 'ejs';
import { escapeHtml } from './utils';
const { collectProductionLicenses } = require('../resources/license-collector.cjs');
@@ -112,6 +112,32 @@ function toLinkedLicenseExpression(rawExpression: string): string {
});
}
function renderLicensePage(filteredPackages: Record<string, PackageInfoWithMetadata>, projName: string, projVersion: string): string {
const rows = Object.entries(filteredPackages).map(([packageName, pack]) => {
const lastAtSignPos = packageName.lastIndexOf('@');
const name = packageName.substring(0, lastAtSignPos);
const version = packageName.substring(lastAtSignPos + 1);
const licenses = escapeHtml(pack.licenseExp || 'N/A');
const linkedName = pack.repository ? `[${escapeHtml(name)}](${pack.repository})` : escapeHtml(name);
return `| ${linkedName} | ${escapeHtml(version)} | ${licenses} |`;
});
return `---
Title: License info, ${escapeHtml(projName)} ${escapeHtml(projVersion)}
---
# License information for ${escapeHtml(projName)} ${escapeHtml(projVersion)}
This page lists all third party libraries the project depends on.
## Libraries
| Name | Version | License |
| --- | --- | --- |
${rows.join('\n')}
`;
}
/**
* Licenses command
*
@@ -165,12 +191,6 @@ Options:
exit(1);
}
const templatePath = path.resolve(__dirname, '../templates/licensePage.ejs');
if (!fs.existsSync(templatePath)) {
console.error(`Cannot find the report template: ${templatePath}`);
exit(1);
}
return new Promise((resolve, reject) => {
// eslint-disable-next-line no-console
console.info(`Checking ${packagePath}`);
@@ -198,28 +218,13 @@ Options:
const packageJson: PackageInfo = getPackageFile(packagePath);
ejs.renderFile(
templatePath,
{
filteredPackages,
projVersion: packageJson.version,
projName: packageJson.name
},
{},
(ejsError: unknown, mdText: string) => {
if (ejsError) {
console.error(ejsError);
reject(ejsError);
} else {
const outputPath = path.resolve(options.outDir || workingDir);
const outputFile = path.join(outputPath, `license-info-${packageJson.version}.md`);
const mdText = renderLicensePage(filteredPackages, packageJson.name, packageJson.version);
const outputPath = path.resolve(options.outDir || workingDir);
const outputFile = path.join(outputPath, `license-info-${packageJson.version}.md`);
fs.writeFileSync(outputFile, mdText);
// eslint-disable-next-line no-console
console.log(`Report saved as ${outputFile}`);
resolve(0);
}
}
);
fs.writeFileSync(outputFile, mdText);
// eslint-disable-next-line no-console
console.log(`Report saved as ${outputFile}`);
resolve(0);
});
}
+20
View File
@@ -0,0 +1,20 @@
/*!
* @license
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}