mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
Replace nopt with commander.js for argument parsing (#7673)
- Replace nopt with commander.js for better command-line argument parsing - Add automatic help generation with detailed descriptions - Maintain backward compatibility with existing arguments - Remove unused nopt dependency from package.json 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
225
app.ts
225
app.ts
@@ -35,12 +35,12 @@ import url from 'node:url';
|
||||
import * as fsSync from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {Command, OptionValues} from 'commander';
|
||||
import compression from 'compression';
|
||||
import express from 'express';
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import morgan from 'morgan';
|
||||
import nopt from 'nopt';
|
||||
import PromClient from 'prom-client';
|
||||
import responseTime from 'response-time';
|
||||
import sanitize from 'sanitize-filename';
|
||||
@@ -92,73 +92,73 @@ import type {Language, LanguageKey} from './types/languages.interfaces.js';
|
||||
|
||||
setBaseDirectory(new URL('.', import.meta.url));
|
||||
|
||||
(nopt as any).invalidHandler = (key: string, val: unknown, types: unknown[]) => {
|
||||
logger.error(
|
||||
`Command line argument type error for "--${key}=${val}",
|
||||
expected ${types.map((t: unknown) => typeof t).join(' | ')}`,
|
||||
);
|
||||
};
|
||||
function parseNumberForOptions(value: string): number {
|
||||
const parsedValue = Number.parseInt(value, 10);
|
||||
if (Number.isNaN(parsedValue)) {
|
||||
throw new Error(`Invalid number: "${value}"`);
|
||||
}
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
type CompilerExplorerOptions = Partial<{
|
||||
interface CompilerExplorerOptions extends OptionValues {
|
||||
env: string[];
|
||||
rootDir: string;
|
||||
host: string;
|
||||
host?: string;
|
||||
port: number;
|
||||
propDebug: boolean;
|
||||
debug: boolean;
|
||||
dist: boolean;
|
||||
archivedVersions: string;
|
||||
noRemoteFetch: boolean;
|
||||
tmpDir: string;
|
||||
wsl: boolean;
|
||||
language: string[];
|
||||
noCache: boolean;
|
||||
ensureNoIdClash: boolean;
|
||||
logHost: string;
|
||||
logPort: number;
|
||||
hostnameForLogging: string;
|
||||
propDebug?: boolean;
|
||||
debug?: boolean;
|
||||
dist?: boolean;
|
||||
remoteFetch: boolean;
|
||||
tmpDir?: string;
|
||||
wsl?: boolean;
|
||||
language?: string[];
|
||||
cache: boolean;
|
||||
ensureNoIdClash?: boolean;
|
||||
logHost?: string;
|
||||
logPort?: number;
|
||||
hostnameForLogging?: string;
|
||||
suppressConsoleLog: boolean;
|
||||
metricsPort: number;
|
||||
loki: string;
|
||||
discoveryonly: string;
|
||||
prediscovered: string;
|
||||
metricsPort?: number;
|
||||
loki?: string;
|
||||
discoveryOnly?: string;
|
||||
prediscovered?: string;
|
||||
webpackContent?: string;
|
||||
local: boolean;
|
||||
version: boolean;
|
||||
webpackContent: string;
|
||||
noLocal: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
// Parse arguments from command line 'node ./app.js args...'
|
||||
const opts = nopt({
|
||||
env: [String, Array],
|
||||
rootDir: [String],
|
||||
host: [String],
|
||||
port: [Number],
|
||||
propDebug: [Boolean],
|
||||
debug: [Boolean],
|
||||
dist: [Boolean],
|
||||
archivedVersions: [String],
|
||||
// Ignore fetch marks and assume every compiler is found locally
|
||||
noRemoteFetch: [Boolean],
|
||||
tmpDir: [String],
|
||||
wsl: [Boolean],
|
||||
// If specified, only loads the specified languages, resulting in faster loadup/iteration times
|
||||
language: [String, Array],
|
||||
// Do not use caching for compilation results (Requests might still be cached by the client's browser)
|
||||
noCache: [Boolean],
|
||||
// Don't cleanly run if two or more compilers have clashing ids
|
||||
ensureNoIdClash: [Boolean],
|
||||
logHost: [String],
|
||||
logPort: [Number],
|
||||
hostnameForLogging: [String],
|
||||
suppressConsoleLog: [Boolean],
|
||||
metricsPort: [Number],
|
||||
loki: [String],
|
||||
discoveryonly: [String],
|
||||
prediscovered: [String],
|
||||
version: [Boolean],
|
||||
webpackContent: [String],
|
||||
noLocal: [Boolean],
|
||||
}) as CompilerExplorerOptions;
|
||||
const program = new Command();
|
||||
program
|
||||
.name('compiler-explorer')
|
||||
.description('Interactively investigate compiler output')
|
||||
.option('--env <environments...>', 'Environment(s) to use', ['dev'])
|
||||
.option('--root-dir <dir>', 'Root directory for config files', './etc')
|
||||
.option('--host <hostname>', 'Hostname to listen on')
|
||||
.option('--port <port>', 'Port to listen on', parseNumberForOptions, 10240)
|
||||
.option('--prop-debug', 'Debug properties')
|
||||
.option('--debug', 'Enable debug output')
|
||||
.option('--dist', 'Running in dist mode')
|
||||
.option('--no-remote-fetch', 'Ignore fetch marks and assume every compiler is found locally')
|
||||
.option('--tmpDir, --tmp-dir <dir>', 'Directory to use for temporary files')
|
||||
.option('--wsl', 'Running under Windows Subsystem for Linux')
|
||||
.option('--language <languages...>', 'Only load specified languages for faster startup')
|
||||
.option('--no-cache', 'Do not use caching for compilation results')
|
||||
.option('--ensure-no-id-clash', "Don't run if compilers have clashing ids")
|
||||
.option('--logHost, --log-host <hostname>', 'Hostname for remote logging')
|
||||
.option('--logPort, --log-port <port>', 'Port for remote logging', parseNumberForOptions)
|
||||
.option('--hostname-for-logging <hostname>', 'Hostname to use in logs')
|
||||
.option('--suppressConsoleLog, --suppress-console-log', 'Disable console logging')
|
||||
.option('--metricsPort, --metrics-port <port>', 'Port to serve metrics on', parseNumberForOptions)
|
||||
.option('--loki <url>', 'URL for Loki logging')
|
||||
.option('--discoveryonly, --discovery-only <file>', 'Output discovery info to file and exit')
|
||||
.option('--prediscovered <file>', 'Input discovery info from file')
|
||||
.option('--webpack-content <dir>', 'Path to webpack content')
|
||||
.option('--no-local', 'Disable local config')
|
||||
.option('--version', 'Show version information');
|
||||
|
||||
program.parse();
|
||||
|
||||
const opts = program.opts<CompilerExplorerOptions>();
|
||||
|
||||
if (opts.debug) logger.level = 'debug';
|
||||
|
||||
@@ -222,39 +222,29 @@ const releaseBuildNumber = (() => {
|
||||
return '';
|
||||
})();
|
||||
|
||||
function patchUpLanguageArg(languages: string[] | undefined): string[] | null {
|
||||
if (!languages) return null;
|
||||
if (languages.length === 1) {
|
||||
// Support old style comma-separated language args.
|
||||
return languages[0].split(',');
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
// Set default values for omitted arguments
|
||||
const defArgs: AppArguments = {
|
||||
rootDir: opts.rootDir || './etc',
|
||||
env: opts.env || ['dev'],
|
||||
const appArgs: AppArguments = {
|
||||
rootDir: opts.rootDir,
|
||||
env: opts.env,
|
||||
hostname: opts.host,
|
||||
port: opts.port || 10240,
|
||||
port: opts.port,
|
||||
gitReleaseName: gitReleaseName,
|
||||
releaseBuildNumber: releaseBuildNumber,
|
||||
wantedLanguages: patchUpLanguageArg(opts.language),
|
||||
doCache: !opts.noCache,
|
||||
fetchCompilersFromRemote: !opts.noRemoteFetch,
|
||||
wantedLanguages: opts.language,
|
||||
doCache: opts.cache,
|
||||
fetchCompilersFromRemote: opts.remoteFetch,
|
||||
ensureNoCompilerClash: opts.ensureNoIdClash,
|
||||
suppressConsoleLog: opts.suppressConsoleLog || false,
|
||||
suppressConsoleLog: opts.suppressConsoleLog,
|
||||
};
|
||||
|
||||
if (opts.logHost && opts.logPort) {
|
||||
logToPapertrail(opts.logHost, opts.logPort, defArgs.env.join('.'), opts.hostnameForLogging);
|
||||
logToPapertrail(opts.logHost, opts.logPort, appArgs.env.join('.'), opts.hostnameForLogging);
|
||||
}
|
||||
|
||||
if (opts.loki) {
|
||||
logToLoki(opts.loki);
|
||||
}
|
||||
|
||||
if (defArgs.suppressConsoleLog) {
|
||||
if (appArgs.suppressConsoleLog) {
|
||||
logger.info('Disabling further console logging');
|
||||
suppressConsoleLog();
|
||||
}
|
||||
@@ -276,12 +266,12 @@ function getFaviconFilename() {
|
||||
|
||||
const propHierarchy = [
|
||||
'defaults',
|
||||
defArgs.env,
|
||||
defArgs.env.map(e => `${e}.${process.platform}`),
|
||||
appArgs.env,
|
||||
appArgs.env.map(e => `${e}.${process.platform}`),
|
||||
process.platform,
|
||||
os.hostname(),
|
||||
].flat();
|
||||
if (!opts.noLocal) {
|
||||
if (opts.local) {
|
||||
propHierarchy.push('local');
|
||||
}
|
||||
logger.info(`properties hierarchy: ${propHierarchy.join(', ')}`);
|
||||
@@ -290,20 +280,20 @@ logger.info(`properties hierarchy: ${propHierarchy.join(', ')}`);
|
||||
if (opts.propDebug) props.setDebug(true);
|
||||
|
||||
// *All* files in config dir are parsed
|
||||
const configDir = path.join(defArgs.rootDir, 'config');
|
||||
const configDir = path.join(appArgs.rootDir, 'config');
|
||||
props.initialize(configDir, propHierarchy);
|
||||
// Instantiate a function to access records concerning "compiler-explorer"
|
||||
// in hidden object props.properties
|
||||
const ceProps = props.propsFor('compiler-explorer');
|
||||
const restrictToLanguages = ceProps<string>('restrictToLanguages');
|
||||
if (restrictToLanguages) {
|
||||
defArgs.wantedLanguages = restrictToLanguages.split(',');
|
||||
appArgs.wantedLanguages = restrictToLanguages.split(',');
|
||||
}
|
||||
|
||||
const languages = (() => {
|
||||
if (defArgs.wantedLanguages) {
|
||||
if (appArgs.wantedLanguages) {
|
||||
const filteredLangs: Partial<Record<LanguageKey, Language>> = {};
|
||||
for (const wantedLang of defArgs.wantedLanguages) {
|
||||
for (const wantedLang of appArgs.wantedLanguages) {
|
||||
for (const lang of Object.values(allLanguages)) {
|
||||
if (lang.id === wantedLang || lang.name === wantedLang || lang.alias.includes(wantedLang)) {
|
||||
filteredLangs[lang.id] = lang;
|
||||
@@ -459,8 +449,7 @@ function oldGoogleUrlHandler(req: express.Request, res: express.Response, next:
|
||||
}
|
||||
|
||||
function startListening(server: express.Express) {
|
||||
const ss = systemdSocket();
|
||||
let _port;
|
||||
const ss: {fd: number} | null = systemdSocket(); // TODO: I'm not sure this works any more
|
||||
if (ss) {
|
||||
// ms (5 min default)
|
||||
const idleTimeout = process.env.IDLE_TIMEOUT;
|
||||
@@ -478,9 +467,15 @@ function startListening(server: express.Express) {
|
||||
server.all('*', reset);
|
||||
logger.info(` IDLE_TIMEOUT: ${idleTimeout}`);
|
||||
}
|
||||
_port = ss;
|
||||
logger.info(` Listening on systemd socket: ${JSON.stringify(ss)}`);
|
||||
server.listen(ss);
|
||||
} else {
|
||||
_port = defArgs.port;
|
||||
logger.info(` Listening on http://${appArgs.hostname || 'localhost'}:${appArgs.port}/`);
|
||||
if (appArgs.hostname) {
|
||||
server.listen(appArgs.port, appArgs.hostname);
|
||||
} else {
|
||||
server.listen(appArgs.port);
|
||||
}
|
||||
}
|
||||
|
||||
const startupGauge = new PromClient.Gauge({
|
||||
@@ -489,24 +484,8 @@ function startListening(server: express.Express) {
|
||||
});
|
||||
startupGauge.set(process.uptime());
|
||||
const startupDurationMs = Math.floor(process.uptime() * 1000);
|
||||
if (Number.isNaN(Number.parseInt(_port))) {
|
||||
// unix socket, not a port number...
|
||||
logger.info(` Listening on socket: //${_port}/`);
|
||||
logger.info(` Startup duration: ${startupDurationMs}ms`);
|
||||
logger.info('=======================================');
|
||||
server.listen(_port);
|
||||
} else {
|
||||
// normal port number
|
||||
logger.info(` Listening on http://${defArgs.hostname || 'localhost'}:${_port}/`);
|
||||
logger.info(` Startup duration: ${startupDurationMs}ms`);
|
||||
logger.info('=======================================');
|
||||
// silly express typing, passing undefined is fine but
|
||||
if (defArgs.hostname) {
|
||||
server.listen(_port, defArgs.hostname);
|
||||
} else {
|
||||
server.listen(_port);
|
||||
}
|
||||
}
|
||||
logger.info(` Startup duration: ${startupDurationMs}ms`);
|
||||
logger.info('=======================================');
|
||||
}
|
||||
|
||||
const awsProps = props.propsFor('aws');
|
||||
@@ -514,31 +493,31 @@ const awsProps = props.propsFor('aws');
|
||||
// eslint-disable-next-line max-statements
|
||||
async function main() {
|
||||
await aws.initConfig(awsProps);
|
||||
SetupSentry(aws.getConfig('sentryDsn'), ceProps, releaseBuildNumber, gitReleaseName, defArgs);
|
||||
SetupSentry(aws.getConfig('sentryDsn'), ceProps, releaseBuildNumber, gitReleaseName, appArgs);
|
||||
const webServer = express();
|
||||
const router = express.Router();
|
||||
|
||||
startWineInit();
|
||||
|
||||
RemoteExecutionQuery.initRemoteExecutionArchs(ceProps, defArgs.env);
|
||||
RemoteExecutionQuery.initRemoteExecutionArchs(ceProps, appArgs.env);
|
||||
|
||||
const formattingService = new FormattingService();
|
||||
await formattingService.initialize(ceProps);
|
||||
|
||||
const clientOptionsHandler = new ClientOptionsHandler(sources, compilerProps, defArgs);
|
||||
const clientOptionsHandler = new ClientOptionsHandler(sources, compilerProps, appArgs);
|
||||
const compilationQueue = CompilationQueue.fromProps(compilerProps.ceProps);
|
||||
const compilationEnvironment = new CompilationEnvironment(
|
||||
compilerProps,
|
||||
awsProps,
|
||||
compilationQueue,
|
||||
formattingService,
|
||||
defArgs.doCache,
|
||||
appArgs.doCache,
|
||||
);
|
||||
const compileHandler = new CompileHandler(compilationEnvironment, awsProps);
|
||||
compilationEnvironment.setCompilerFinder(compileHandler.findCompiler.bind(compileHandler));
|
||||
const storageType = getStorageTypeByKey(storageSolution);
|
||||
const storageHandler = new storageType(httpRoot, compilerProps, awsProps);
|
||||
const compilerFinder = new CompilerFinder(compileHandler, compilerProps, defArgs, clientOptionsHandler);
|
||||
const compilerFinder = new CompilerFinder(compileHandler, compilerProps, appArgs, clientOptionsHandler);
|
||||
|
||||
const isExecutionWorker = ceProps<boolean>('execqueue.is_worker', false);
|
||||
const healthCheckFilePath = ceProps('healthCheckFilePath', null) as string | null;
|
||||
@@ -576,7 +555,7 @@ async function main() {
|
||||
if (!isExecutionWorker && initialCompilers.length === 0) {
|
||||
throw new Error('Unexpected failure, no compilers found!');
|
||||
}
|
||||
if (defArgs.ensureNoCompilerClash) {
|
||||
if (appArgs.ensureNoCompilerClash) {
|
||||
logger.warn('Ensuring no compiler ids clash');
|
||||
if (initialFindResults.foundClash) {
|
||||
// If we are forced to have no clashes, throw an error with some explanation
|
||||
@@ -586,7 +565,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.discoveryonly) {
|
||||
if (opts.discoveryOnly) {
|
||||
for (const compiler of initialCompilers) {
|
||||
if (compiler.buildenvsetup && compiler.buildenvsetup.id === '') delete compiler.buildenvsetup;
|
||||
|
||||
@@ -597,8 +576,8 @@ async function main() {
|
||||
compiler.cachedPossibleArguments = compilerInstance.possibleArguments.possibleArguments;
|
||||
}
|
||||
}
|
||||
await fs.writeFile(opts.discoveryonly, JSON.stringify(initialCompilers));
|
||||
logger.info(`Discovered compilers saved to ${opts.discoveryonly}`);
|
||||
await fs.writeFile(opts.discoveryOnly, JSON.stringify(initialCompilers));
|
||||
logger.info(`Discovered compilers saved to ${opts.discoveryOnly}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -607,7 +586,7 @@ async function main() {
|
||||
clientOptionsHandler,
|
||||
renderConfig,
|
||||
storageHandler,
|
||||
defArgs.wantedLanguages?.[0],
|
||||
appArgs.wantedLanguages?.[0],
|
||||
);
|
||||
const routeApi = new RouteAPI(router, {
|
||||
compileHandler,
|
||||
@@ -615,7 +594,7 @@ async function main() {
|
||||
storageHandler,
|
||||
compilationEnvironment,
|
||||
ceProps,
|
||||
defArgs,
|
||||
defArgs: appArgs,
|
||||
renderConfig,
|
||||
renderGoldenLayout,
|
||||
});
|
||||
@@ -649,7 +628,7 @@ async function main() {
|
||||
|
||||
if (opts.metricsPort) {
|
||||
logger.info(`Running metrics server on port ${opts.metricsPort}`);
|
||||
setupMetricsServer(opts.metricsPort, defArgs.hostname);
|
||||
setupMetricsServer(opts.metricsPort, appArgs.hostname);
|
||||
}
|
||||
|
||||
webServer
|
||||
@@ -851,7 +830,7 @@ async function main() {
|
||||
noscriptHandler.initializeRoutes();
|
||||
routeApi.initializeRoutes();
|
||||
|
||||
if (!defArgs.doCache) {
|
||||
if (!appArgs.doCache) {
|
||||
logger.info(' with disabled caching');
|
||||
}
|
||||
setupEventLoopLagLogging();
|
||||
|
||||
Reference in New Issue
Block a user