@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
375 lines (354 loc) • 11.6 kB
JavaScript
const { read, write, exists, yaml } = require('../cds').utils;
const cds = require("../cds");
const { readJSONC } = require("../util/fs");
const path = require("path");
const term = require("../util/term");
const JSONC = require('../util/jsonc');
const { COMMAND_INIT } = require("../init/constants");
module.exports = {
/**
* Reads ESLint config contents from file
* @param {*} configPath config file path to read from
* @returns {object[] | object} config file contents
*/
async readEslintConfigLegacy(configPath) {
let config;
const configFile = path.basename(configPath);
const configString = await read(configPath, "utf8");
switch (configFile) {
case ".eslintrc.js":
case ".eslintrc.cjs":
try {
config = JSONC.parse((configString.replace(/^module\.exports =/, "")));
} catch {
config = {};
}
break;
case ".eslintrc.yaml":
case ".eslintrc.yml":
config = yaml.parse(await read(configPath, "utf8"));
break;
case ".eslintrc.json":
case ".eslintrc":
config = await readJSONC(configPath);
break;
case "package.json":
config = await read(configPath, "json");
if ("eslintConfig" in config) {
config = config["eslintConfig"];
} else {
config = {}
}
break;
default:
break;
}
return config;
},
/**
* Reads ESLint config contents from file
* @param {*} configPath config file path to read from
* @returns config file contents
*/
async readEslintConfig(configPath) {
let config;
switch (path.basename(configPath)) {
case "eslint.config.mjs":
try { config = await import (configPath) } catch { config = null }
break
case "eslint.config.js":
case "eslint.config.cjs":
try { config = require (configPath) } catch { config = null }
}
return config;
},
/**
* Checks ESLint config file contents for locally installed
* plugin with:
* (1) All CDS recommended rules "on"
* (2) Add's custom rule example 'no-entity-moo' if requested
* @param {*} configPath ESLint config file
* @returns {void}
*/
async sanitizeEslintConfig(configIn, customRuleExample = false, logger = console) {
let configContents;
const isFilePath = typeof configIn === "string";
if (isFilePath) {
configContents = await this.readEslintConfig(configIn)
}
configContents ??= { ...configIn };
if (customRuleExample) {
const ruleName = "no-entity-moo";
const ruleValue = 2;
if ("rules" in configContents) {
configContents["rules"][ruleName] = ruleValue;
} else {
configContents["rules"] = { [ruleName]: ruleValue };
}
}
if (isFilePath) {
await this.writeEslintConfig(configIn, cds.cli?.command === COMMAND_INIT, logger);
}
return configContents;
},
/**
* Checks ESLint config file contents for locally installed
* plugin with:
* (1) All CDS recommended rules "on"
* (2) Option "root": false to stop further config merging
* (3) Add's custom rule example 'no-entity-moo' if requested
* @param {*} configPath ESLint config file
*/
async sanitizeEslintConfigLegacy(configIn, customRuleExample = false, logger = console) {
let configContents = {};
const isFilePath = typeof configIn === "string";
if (isFilePath) {
if (exists(configIn)) {
configContents = (await this.readEslintConfig(configIn)) || {};
}
} else {
configContents = { ...configIn };
}
const configType = "recommended";
const extendsValue = `plugin:@sap/cds/${configType}`;
if ("extends" in configContents) {
if (Array.isArray(configContents["extends"])) {
if (!configContents["extends"].includes(extendsValue)) {
configContents["extends"].push(extendsValue);
}
} else if (configContents["extends"] !== extendsValue) {
configContents["extends"] = [configContents["extends"], extendsValue];
}
} else {
configContents["extends"] = extendsValue;
}
if ("root" in configContents) {
configContents["root"] = false;
}
if (customRuleExample) {
const ruleName = "no-entity-moo";
const ruleValue = 2;
if ("rules" in configContents) {
configContents["rules"][ruleName] = ruleValue;
} else {
configContents["rules"] = { [ruleName]: ruleValue };
}
}
if (isFilePath) {
await this.writeEslintConfigLegacy(configIn, configContents, logger);
}
return configContents;
},
/**
* Writes ESLint config contents to file
* @param {*} configContents config file contents
* @param {*} configPath config file path to write to
* @returns {Promise<void>}
*/
async writeEslintConfigLegacy(configPath, configContents, logger = console) {
const configType = "recommended-legacy";
const extendsValue = `plugin:@sap/cds/${configType}`;
let contents = {};
let configFile = path.basename(configPath);
let configToAdd = {};
switch (configFile) {
case ".eslintrc.js":
case ".eslintrc.cjs":
try {
configContents = await this.readEslintConfigLegacy(configPath);
} catch (err) {
configContents = {};
}
if (configContents && configContents["extends"]) {
if (
!configContents["extends"] === extendsValue ||
!configContents["extends"].includes(extendsValue)
) {
configToAdd["extends"] = extendsValue;
}
} else {
configToAdd["extends"] = extendsValue;
}
if (configToAdd["extends"]) {
logger.log(
`${term.warn(
`\n\nPlease add the following to your "${configFile}":\n\nmodule.exports = ${JSON.stringify(configToAdd, null, 2)}`
)}\n`
);
}
break;
case ".eslintrc.yaml":
case ".eslintrc.yml":
await write(configPath, yaml.stringify(configContents));
break;
case ".eslintrc.json":
case ".eslintrc":
await write(configPath, configContents, { spaces: 2 });
break;
case "package.json":
if (exists(configPath)) {
contents = await read(configPath, "json");
} else {
contents = {};
}
if (!("eslintConfig" in configContents)) {
contents["eslintConfig"] = configContents;
} else {
contents["eslintConfig"] = [...configContents, ...contents["eslintConfig"]];
}
await write(configPath, contents, { spaces: 2 });
break;
default:
break;
}
},
/**
* Suggest ESLint config contents to file
* @param {*} configContents config file contents
* @param {*} configPath config file path to write to
* @returns {Promise<void>}
*/
async writeEslintConfig(configPath, force = false, logger = console) {
let configFile = path.basename(configPath);
switch (configFile) {
case "eslint.config.js":
case "eslint.config.cjs":
if (!force && exists(configPath)) {
logger.log(
`${term.warn(
`\n\nPlease add the following to your "${configFile}":` +
'\n\n 1. At the top of your file, import the following:')}` +
`\n\n ${term.bold('const cds = require(\'@sap/eslint-plugin-cds\')')}` +
`${term.warn(
'\n\n 2. In your export, add the following to the top of the array:')}` +
`\n\n ${term.bold('cds.configs.recommended')}`
);
} else {
await write(configPath, `
const cds = require('@sap/eslint-plugin-cds')
module.exports = [
cds.configs.recommended,
{
plugins: {
'@sap/cds': cds
},
files: [
...cds.configs.recommended.files
],
rules: {
...cds.configs.recommended.rules
}
}
]
`)
}
break;
case "eslint.config.mjs":
if (!force && exists(configPath)) {
logger.log(
`${term.warn(
`\n\nPlease add the following to your "${configFile}":` +
'\n\n 1. At the top of your file, import the following:')}` +
`\n\n ${term.bold('import cdsPlugin from \'@sap/eslint-plugin-cds\'')}` +
`${term.warn(
'\n\n 2. In your export, add the following to the front of the array:')}` +
`\n\n ${term.bold('cdsPlugin.configs.recommended')}`
);
} else {
await write(configPath, `
import cds from '@sap/cds/eslint.config.mjs'
import cdsPlugin from '@sap/eslint-plugin-cds'
export default [...cds.recommended, cdsPlugin.configs.recommended]
`)
}
break;
default:
break;
}
},
/**
* Read VS Code settings from file
* @param {*} settingsPath settings file path to read from
* @returns settings file contents
*/
async readVscodeSettings(settingsPath) {
let settings;
if (exists(settingsPath)) {
try {
settings = await readJSONC(settingsPath);
} catch (err) {
settings = {};
}
}
return settings;
},
/**
* Merges two arrays into one
* @param {*} array input array
* @param {*} arrayToMerge array to merge with
* @returns merged array (without duplicates)
*/
mergeArrays(array, arrayToMerge) {
let arrayMerged = array.concat(arrayToMerge);
if (typeof array === "string") {
array = [array];
}
if (typeof arrayToMerge === "string") {
arrayToMerge = [arrayToMerge];
}
arrayMerged = [...new Set([...array, ...arrayToMerge])];
return arrayMerged;
},
/**
* Add VS Code ESLint extension settings required for CDS linting:
* (1) Extension file types: [cds, csn]
* (2) "Custom" case adds rulePaths and removes configFile
* (3) "Global" case adds configFile and removes rulePaths
* @param {*} settingsPath VS Code settings file
*/
async sanitizeVscodeSettings(settingsPath, lintType, lintFileTypes, customRuleExample = false) {
let settings = (await this.readVscodeSettings(settingsPath)) || {};
const rulePaths = [path.join(".eslint/rules")];
if (settings["eslint.validate"]) {
settings["eslint.validate"] = this.mergeArrays(settings["eslint.validate"], lintFileTypes);
} else {
settings["eslint.validate"] = lintFileTypes;
}
if (lintType === "local") {
if (settings["eslint.options"]) {
if (customRuleExample) {
settings["eslint.options"]["rulePaths"] = rulePaths;
}
} else {
if (customRuleExample) {
settings["eslint.options"] = {
rulePaths: rulePaths,
};
}
}
}
await write(settingsPath, settings, { spaces: 2 });
},
/**
* Joins or creates prescribed options with that of user
* - Value for 'config' key will be overwritten if assigned by `cds lint`
* @param {*} propKey option key property
* @param {*} propsToAdd option values to add for key property
* @returns {void}
*/
mergeWithUserOpts(userOpts, propKey = "", propsToAdd = "") {
if (propsToAdd) {
if (Object.keys(userOpts).includes(propKey)) {
if (!["config", "format"].includes(propKey)) {
propsToAdd = this.mergeArrays(userOpts[propKey].split(","), propsToAdd.split(",")).join(
","
);
}
}
userOpts[propKey] = propsToAdd;
} else {
userOpts[propKey] = true;
}
return userOpts;
},
};