UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

375 lines (354 loc) 11.6 kB
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; }, };