license-kit
Version:
Aggregate license notes of OSS libraries used in your Node.js project, analyze & visualize OSS licenses with AI-turbocharged tooling
174 lines (173 loc) • 7.94 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = visualizeCommandSetup;
const node_fs_1 = require("node:fs");
const node_path_1 = __importDefault(require("node:path"));
const node_url_1 = require("node:url");
const commander_1 = require("commander");
const express_1 = __importDefault(require("express"));
const helmet_1 = __importDefault(require("helmet"));
const next_1 = __importDefault(require("next"));
const open_1 = __importDefault(require("open"));
const signale_1 = require("signale");
const generateLicensesMapping_1 = require("../logic/generateLicensesMapping");
const commandUtils_1 = require("../utils/commandUtils");
const projectUtils_1 = require("../utils/projectUtils");
const isDev = !!process.env.NODE_ENV && process.env.NODE_ENV !== 'production';
const visualizerSignale = new signale_1.Signale({ scope: 'visualize' });
const sseSignale = new signale_1.Signale({ scope: 'SSE' });
const apiSignale = new signale_1.Signale({ scope: 'API' });
const reportSignale = new signale_1.Signale({ scope: 'report' });
function visualizeCommandSetup(program) {
return (0, commandUtils_1.curryCommonScanOptions)(program
.command('visualize')
.description('Launches a local server providing a web license graph visualizer & analyzer app: summarizes the dependency graph state, shows an interactive graph of licenses with possibility to select a subgraph, provides browser built-in AI-turbocharged summary of the dependency graph.')
.option('--port [port]', 'Port on which to launch the app', (value) => {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new commander_1.InvalidArgumentError('Not a number.');
}
return parsedValue;
}, 8094)
.option('--h, --host [host]', 'Host on which to launch the app', 'localhost')
.option('--a, --auto-open [open]', 'Host on which to launch the app', (value) => value === 'true' || value === '1', true)
.option('--root [path]', 'Path to the root of your project', '.')).action(async (options) => {
(0, commandUtils_1.validateCommonScanOptions)(options);
const expressApp = (0, express_1.default)();
expressApp.use((0, helmet_1.default)({
contentSecurityPolicy: false,
}));
const eventClients = new Set();
/** Logic - begin */
let lastScanPackageJsonChecksum = null;
let lastScanResult = null;
function updateLastScanResultIfNeeded() {
let result;
const currentPackageJsonChecksum = (0, projectUtils_1.getPackageLockChecksum)(options);
if (lastScanResult && lastScanPackageJsonChecksum === currentPackageJsonChecksum) {
result = lastScanResult;
return false;
}
else {
if (lastScanResult) {
reportSignale.log(`Project's root ${node_path_1.default.basename((0, projectUtils_1.getLockfilePath)(options))} changed (new checksum: ${currentPackageJsonChecksum}), re-generating report...`);
}
else {
reportSignale.log('Generating report for the first time, please stand by...');
}
result = (0, generateLicensesMapping_1.generateLicensesMapping)(options);
reportSignale.log('Report generated');
lastScanResult = result;
lastScanPackageJsonChecksum = currentPackageJsonChecksum;
return true;
}
}
/** Logic - end */
/** API routes - begin */
expressApp.get('/api/report', (req, res) => {
if (!lastScanResult) {
updateLastScanResultIfNeeded();
}
const { licenses, projectName } = lastScanResult;
res.json({
report: licenses,
projectName,
});
});
expressApp.get('/api/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.flushHeaders();
eventClients.add(res);
req.on('close', () => {
res.end();
});
});
/** API routes - end */
/** Lockfile watcher - begin */
const lockfilePath = (0, projectUtils_1.getLockfilePath)(options);
visualizerSignale.log(`Watching '${lockfilePath}' file for changes`);
const packageJsonWatcher = (0, node_fs_1.watch)(lockfilePath, (event) => {
if (event === 'change') {
const didChange = updateLastScanResultIfNeeded();
if (didChange) {
sseSignale.log('Sending report updates to clients over SSE');
eventClients.forEach((client) => {
client.write(`event: message\ndata: ${JSON.stringify({
type: 'UPDATE',
report: lastScanResult.licenses,
projectName: lastScanResult.projectName,
})}\n\n`);
});
}
}
});
/** Lockfile watcher - end */
let server;
await new Promise((resolve) => {
server = expressApp.listen(options.port, options.host, async () => {
apiSignale.log('Preparing GUI server...');
// Next app
const visualizerNextAppDir = node_path_1.default.join(__dirname, '..', '..', ...(isDev ? ['..', 'visualizer'] : []));
const visualizerNextApp = (0, next_1.default)({
dev: isDev,
dir: visualizerNextAppDir,
customServer: true,
httpServer: server,
hostname: options.host,
port: options.port,
});
await visualizerNextApp.prepare();
const visualizerReqHandler = visualizerNextApp.getRequestHandler();
expressApp.use((req, res) => {
visualizerReqHandler(req, res, (0, node_url_1.parse)(req.url, true));
});
apiSignale.log(`Server running at http://${options.host}:${options.port}\n`);
// open the browser automatically
if (options.autoOpen) {
await (0, open_1.default)(`http://${options.host}:${options.port}`);
}
resolve();
});
});
const threadBlocker = (() => {
let resolver;
const promise = new Promise((resolve) => {
resolver = resolve;
});
return {
resolve: resolver,
promise,
};
})();
function shutdown() {
eventClients.forEach((client) => client.end());
server?.close();
packageJsonWatcher.close();
process.exit(0);
}
process.on('SIGINT', () => {
threadBlocker.resolve();
shutdown();
});
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', (inputBuff) => {
const input = inputBuff.toString().trim().toLowerCase();
if (input === 'q' || input === '\u0003') {
threadBlocker.resolve();
}
});
visualizerSignale.log('Press "q" or ctrl+c to stop the server');
await threadBlocker.promise;
visualizerSignale.log('Stopping the server...');
shutdown();
});
}