@sonar/scan
Version:
SonarQube/SonarCloud Scanner for the JavaScript world
415 lines (414 loc) • 18.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getHostProperties = getHostProperties;
exports.getProperties = getProperties;
/*
* sonar-scanner-npm
* Copyright (C) 2022-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const properties_file_1 = require("properties-file");
const proxy_from_env_1 = require("proxy-from-env");
const slugify_1 = __importDefault(require("slugify"));
const constants_1 = require("./constants");
const logging_1 = require("./logging");
const platform_1 = require("./platform");
const types_1 = require("./types");
function getDefaultProperties() {
return {
[types_1.ScannerProperty.SonarUserHome]: path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? '', constants_1.SONAR_DIR_DEFAULT),
[types_1.ScannerProperty.SonarWorkingDirectory]: '.scannerwork',
[types_1.ScannerProperty.SonarScannerOs]: (0, platform_1.getSupportedOS)(),
[types_1.ScannerProperty.SonarScannerArch]: (0, platform_1.getArch)(),
};
}
/**
* Convert the name of a sonar property from its environment variable form
* (eg SONAR_SCANNER_FOO_BAR) to its sonar form (eg sonar.scanner.fooBar).
*/
function envNameToSonarPropertyNameMapper(envName) {
// Extract the name and convert to camel case
const sonarScannerKey = envName
.substring(constants_1.ENV_VAR_PREFIX.length)
.toLowerCase()
.replace(/_([a-z])/g, g => g[1].toUpperCase());
return `sonar.scanner.${sonarScannerKey}`;
}
/**
* Convert the name of a sonar property from its environment variable form
* (eg npm_config_sonar_scanner_) to its sonar form (eg sonar.scanner.fooBar).
*/
function npmConfigEnvNameToSonarPropertyNameMapper(envName) {
// Extract the name and convert to camel case
const sonarScannerKey = envName
.substring(constants_1.NPM_CONFIG_ENV_VAR_PREFIX.length)
.toLowerCase()
.replace(/_([a-z])/g, g => g[1].toUpperCase());
return `sonar.scanner.${sonarScannerKey}`;
}
/**
* Build the config.
*/
function getPackageJsonProperties(projectBaseDir, sonarBaseExclusions) {
const pkg = readPackageJson(projectBaseDir);
if (!pkg) {
return {
[types_1.ScannerProperty.SonarExclusions]: sonarBaseExclusions,
};
}
(0, logging_1.log)(logging_1.LogLevel.INFO, 'Retrieving info from "package.json" file');
const packageJsonParams = {
[types_1.ScannerProperty.SonarExclusions]: sonarBaseExclusions,
};
populatePackageParams(packageJsonParams, pkg);
populateCoverageParams(packageJsonParams, pkg, projectBaseDir, sonarBaseExclusions);
populateTestExecutionParams(packageJsonParams, pkg, projectBaseDir);
return packageJsonParams;
}
function readPackageJson(projectBaseDir) {
const packageFile = path_1.default.join(projectBaseDir, 'package.json');
try {
const packageData = fs_extra_1.default.readFileSync(packageFile).toString();
return JSON.parse(packageData);
}
catch (error) {
(0, logging_1.log)(logging_1.LogLevel.INFO, `Unable to read "package.json" file`);
return null;
}
}
function fileExistsInProjectSync(projectBaseDir, file) {
return fs_extra_1.default.existsSync(path_1.default.join(projectBaseDir, file));
}
function dependenceExists(pkg, pkgName) {
return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) {
const dependencyGroup = pkg[prop];
return (typeof dependencyGroup === 'object' && dependencyGroup !== null && pkgName in dependencyGroup);
});
}
function populatePackageParams(params, pkg) {
const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g;
if (pkg.name) {
params['sonar.projectKey'] = (0, slugify_1.default)(pkg.name, {
remove: invalidCharacterRegex,
});
params['sonar.projectName'] = pkg.name;
}
if (pkg.version) {
params['sonar.projectVersion'] = pkg.version;
}
if (pkg.description) {
params['sonar.projectDescription'] = pkg.description;
}
if (pkg.homepage) {
params['sonar.links.homepage'] = pkg.homepage;
}
if (pkg.bugs?.url) {
params['sonar.links.issue'] = pkg.bugs.url;
}
if (pkg.repository?.url) {
params['sonar.links.scm'] = pkg.repository.url;
}
}
function populateCoverageParams(params, pkg, projectBaseDir, sonarBaseExclusions) {
const potentialCoverageDirs = [
// nyc coverage output directory
// See: https://github.com/istanbuljs/nyc#configuring-nyc
pkg['nyc']?.['report-dir'],
// jest coverage output directory
// See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string
pkg['jest']?.['coverageDirectory'],
]
.filter(Boolean)
.concat(
// default coverage output directory
'coverage');
const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs));
params[types_1.ScannerProperty.SonarExclusions] = sonarBaseExclusions;
for (const lcovReportDir of uniqueCoverageDirs) {
const lcovReportPath = lcovReportDir && path_1.default.posix.join(lcovReportDir, 'lcov.info');
if (lcovReportPath && fileExistsInProjectSync(projectBaseDir, lcovReportPath)) {
params[types_1.ScannerProperty.SonarExclusions] +=
(params[types_1.ScannerProperty.SonarExclusions].length > 0 ? ',' : '') +
path_1.default.posix.join(lcovReportDir, '**');
// TODO: (SCANNPM-34) use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format
params['sonar.javascript.lcov.reportPaths'] = lcovReportPath;
// https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/
}
}
}
function populateTestExecutionParams(params, pkg, projectBaseDir) {
if (dependenceExists(pkg, 'mocha-sonarqube-reporter') &&
fileExistsInProjectSync(projectBaseDir, 'xunit.xml')) {
// https://docs.sonarqube.org/display/SONAR/Generic+Test+Data
params['sonar.testExecutionReportPaths'] = 'xunit.xml';
// TODO: (SCANNPM-13) use `glob` to lookup xunit format files and transformation to sonar generic report format
}
}
/**
* Convert CLI args into scanner properties.
*/
function getCommandLineProperties(cliArgs) {
const properties = {};
if (cliArgs?.debug) {
properties[types_1.ScannerProperty.SonarVerbose] = 'true';
}
const { define } = cliArgs ?? {};
if (!define || define.length === 0) {
return properties;
}
// Parse CLI args (eg: -Dsonar.token=xxx)
for (const arg of define) {
const [key, ...value] = arg.split('=');
properties[key] = value.join('=');
}
return properties;
}
/**
* Parse properties stored in sonar project properties file, if it exists.
* Return an empty object if the file does not exist.
*/
function getSonarFileProperties(projectBaseDir) {
// Read sonar project properties file in project base dir
try {
const sonarPropertiesFile = path_1.default.join(projectBaseDir, constants_1.SONAR_PROJECT_FILENAME);
const data = fs_extra_1.default.readFileSync(sonarPropertiesFile);
return (0, properties_file_1.getProperties)(data);
}
catch (error) {
(0, logging_1.log)(logging_1.LogLevel.DEBUG, `Failed to read ${constants_1.SONAR_PROJECT_FILENAME} file: ${error}`);
return {};
}
}
/**
* Get scanner properties from scan option object (JS API).
*/
function getScanOptionsProperties(scanOptions) {
const properties = {
...scanOptions.options,
};
if (typeof scanOptions.serverUrl !== 'undefined') {
properties[types_1.ScannerProperty.SonarHostUrl] = scanOptions.serverUrl;
}
if (typeof scanOptions.token !== 'undefined') {
properties[types_1.ScannerProperty.SonarToken] = scanOptions.token;
}
if (typeof scanOptions.verbose !== 'undefined') {
properties[types_1.ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false';
}
if (typeof scanOptions.version !== 'undefined') {
properties[types_1.ScannerProperty.SonarScannerCliVersion] = scanOptions.version;
}
return properties;
}
/**
* Automatically parse properties from environment variables.
*/
function getEnvironmentProperties() {
const { env } = process;
const jsonEnvVariables = ['SONAR_SCANNER_JSON_PARAMS', 'SONARQUBE_SCANNER_PARAMS'];
let properties = {};
// Get known environment variables
for (const [envName, scannerProperty] of constants_1.ENV_TO_PROPERTY_NAME) {
if (envName in env) {
const envValue = env[envName];
if (typeof envValue !== 'undefined') {
properties[scannerProperty] = envValue;
}
}
}
// Get generic environment variables
properties = {
...properties,
...Object.fromEntries(Object.entries(env)
.filter(([key]) => key.startsWith(constants_1.NPM_CONFIG_ENV_VAR_PREFIX))
.filter(([key]) => !jsonEnvVariables.includes(key))
.map(([key, value]) => [npmConfigEnvNameToSonarPropertyNameMapper(key), value])),
...Object.fromEntries(Object.entries(env)
.filter(([key]) => key.startsWith(constants_1.ENV_VAR_PREFIX))
.filter(([key]) => !jsonEnvVariables.includes(key))
.map(([key, value]) => [envNameToSonarPropertyNameMapper(key), value])),
};
// Get JSON parameters from env
try {
const jsonParams = env.SONAR_SCANNER_JSON_PARAMS ?? env.SONARQUBE_SCANNER_PARAMS;
if (jsonParams) {
properties = {
...JSON.parse(jsonParams),
...properties,
};
}
if (!env.SONAR_SCANNER_JSON_PARAMS && env.SONARQUBE_SCANNER_PARAMS) {
(0, logging_1.log)(logging_1.LogLevel.WARN, 'SONARQUBE_SCANNER_PARAMS is deprecated, please use SONAR_SCANNER_JSON_PARAMS instead');
}
}
catch (e) {
(0, logging_1.log)(logging_1.LogLevel.WARN, `Failed to parse JSON parameters from ENV: ${e}`);
}
return properties;
}
/**
* Get bootstrapper properties, that can not be overridden.
*/
function getBootstrapperProperties(startTimestampMs) {
return {
'sonar.scanner.app': constants_1.SCANNER_BOOTSTRAPPER_NAME,
'sonar.scanner.appVersion': '4.3.2',
'sonar.scanner.bootstrapStartTime': startTimestampMs.toString(),
// These cache statuses are set during the bootstrapping process.
// We already set them here to prevent them from being overridden.
'sonar.scanner.wasJreCacheHit': types_1.CacheStatus.Disabled,
'sonar.scanner.wasEngineCacheHit': 'false',
};
}
/**
* Get endpoint properties from scanner properties.
*/
function getHostProperties(properties) {
const sonarHostUrl = properties[types_1.ScannerProperty.SonarHostUrl]?.replace(/\/$/, '')?.trim();
const sonarApiBaseUrl = properties[types_1.ScannerProperty.SonarScannerApiBaseUrl];
const sonarCloudSpecified = isSonarCloud(properties, sonarHostUrl);
if (!sonarHostUrl || sonarCloudSpecified) {
const region = (properties[types_1.ScannerProperty.SonarRegion] ?? '').toLowerCase();
if (isSonarCloudUS(sonarHostUrl) || region === constants_1.REGION_US) {
return {
[types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true',
[types_1.ScannerProperty.SonarHostUrl]: properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] ?? constants_1.SONARCLOUD_URL_US,
[types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? constants_1.SONARCLOUD_API_BASE_URL_US,
};
}
else if (isSonarCloudEU(sonarHostUrl) || region === '') {
return {
[types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true',
[types_1.ScannerProperty.SonarHostUrl]: properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] ?? constants_1.SONARCLOUD_URL,
[types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? constants_1.SONARCLOUD_API_BASE_URL,
};
}
else {
const regionsPrint = constants_1.REGIONS.map(r => `"${r}"`);
throw new Error(`Unsupported region '${region}'. List of supported regions: ${regionsPrint}. Please check the '${types_1.ScannerProperty.SonarRegion}' property or the 'SONAR_REGION' environment variable.`);
}
}
else {
return {
[types_1.ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false',
[types_1.ScannerProperty.SonarHostUrl]: sonarHostUrl,
[types_1.ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? `${sonarHostUrl}/api/v2`,
};
}
}
function isSonarCloud(properties, sonarHostUrl) {
return (properties[types_1.ScannerProperty.SonarScannerSonarCloudUrl] === sonarHostUrl ||
isSonarCloudEU(sonarHostUrl) ||
isSonarCloudUS(sonarHostUrl));
}
function isSonarCloudEU(sonarHostUrl) {
return constants_1.SONARCLOUD_URL_REGEX.exec(sonarHostUrl ?? '');
}
function isSonarCloudUS(sonarHostUrl) {
return constants_1.SONARCLOUD_US_URL_REGEX.exec(sonarHostUrl ?? '');
}
function getHttpProxyEnvProperties(serverUrl) {
const proxyUrl = (0, proxy_from_env_1.getProxyForUrl)(serverUrl);
// If no proxy is set, return the properties as is
if (!proxyUrl) {
return {};
}
// Parse the proxy URL
const url = new URL(proxyUrl);
const properties = {};
properties[types_1.ScannerProperty.SonarScannerProxyHost] = url.hostname;
if (url.port) {
properties[types_1.ScannerProperty.SonarScannerProxyPort] = url.port;
}
if (url.username) {
properties[types_1.ScannerProperty.SonarScannerProxyUser] = url.username;
}
if (url.password) {
properties[types_1.ScannerProperty.SonarScannerProxyPassword] = url.password;
}
return properties;
}
function hotfixDeprecatedProperties(properties) {
for (const [oldProp, newProp] of constants_1.SCANNER_DEPRECATED_PROPERTIES) {
if (typeof properties[oldProp] !== 'undefined') {
if (typeof properties[newProp] === 'undefined') {
(0, logging_1.log)(logging_1.LogLevel.WARN, `Property "${oldProp}" is deprecated and will be removed in a future version. Please use "${newProp}" instead.`);
properties[newProp] = properties[oldProp];
}
else {
(0, logging_1.log)(logging_1.LogLevel.WARN, `Both properties "${oldProp}" and "${newProp}" are set. "${oldProp}" is deprecated and will be removed in a future version. Value of deprecated property "${oldProp}" will be ignored.`);
properties[oldProp] = properties[newProp];
}
}
}
return properties;
}
function normalizeProperties(properties) {
for (const [key, value] of Object.entries(properties)) {
if (value === null) {
properties[key] = '';
}
else if (typeof value === 'undefined') {
delete properties[key];
}
else {
properties[key] = value.toString().trim();
}
}
return properties;
}
function getProperties(scanOptions, startTimestampMs, cliArgs) {
const envProperties = getEnvironmentProperties();
const scanOptionsProperties = getScanOptionsProperties(scanOptions);
const cliProperties = getCommandLineProperties(cliArgs);
const userProperties = {
...envProperties,
...scanOptionsProperties,
...cliProperties,
};
// Compute default base dir and exclusions respecting order of precedence we use for the final merge
const projectBaseDir = userProperties[types_1.ScannerProperty.SonarProjectBaseDir] ?? process.cwd();
const baseSonarExclusions = userProperties[types_1.ScannerProperty.SonarExclusions] ?? constants_1.DEFAULT_SONAR_EXCLUSIONS;
// Infer specific properties from project files
const inferredProperties = {
...getPackageJsonProperties(projectBaseDir, baseSonarExclusions),
...getSonarFileProperties(projectBaseDir),
};
// Generate proxy properties from HTTP[S]_PROXY env variables, if not already set
const httpProxyProperties = getHttpProxyEnvProperties(userProperties[types_1.ScannerProperty.SonarHostUrl]);
// Merge properties respecting order of precedence
let properties = {
...getDefaultProperties(), // fallback to default if nothing was provided for these properties
...inferredProperties,
...httpProxyProperties,
...userProperties, // Highest precedence
};
properties = hotfixDeprecatedProperties({
...properties,
// Can't be overridden:
...getHostProperties(properties), // Hotfix host properties with custom SonarCloud URL
...getBootstrapperProperties(startTimestampMs),
'sonar.projectBaseDir': projectBaseDir,
});
properties = normalizeProperties(properties);
return properties;
}