@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
226 lines (205 loc) • 7.01 kB
JavaScript
const os = require("os");
const path = require("path");
const cp = require("child_process");
const term = require("../../../util/term");
const checks = require("../../../lint/checks");
const io = require("../../../lint/io");
const readline = require("readline");
const { renderAndCopy } = require('../../template')
const { LINT_DEV, LINT_GLOBAL } = require("../../constants").OPTIONS;
const { COMMAND_INIT } = require("../../constants");
const cds = require("../../../cds");
const { mkdirp, exists } = cds.utils;
const IS_WIN = os.platform() === "win32";
const VSCODE_FILE_ASSOCIATIONS = {
"csv": ["csv", "csv (semicolon)", "tsv", "tab"]
}
module.exports = class LintTemplate extends require('../../plugin') {
static help() {
return 'configure cds lint'
}
constructor() {
super();
this.customPath = path.join(cds.root, ".eslint");
this.docsPath = path.join(this.customPath, "docs");
this.rulesPath = path.join(this.customPath, "rules");
this.testsPath = path.join(this.customPath, "tests");
this.lintType = "local";
this.customRuleExample = false;
this.missingNpmDependencies = { names: [], msgs: [] };
this.isTest = false;
}
async canRun() {
const { lint } = cds.cli.options
if (lint) {
// If custom rule example is requested, also trigger
// "custom" rule type of setup
if (lint.has(LINT_DEV)) {
this.customRuleExample = true;
}
if (lint.has(LINT_GLOBAL)) {
this.lintType = "global";
}
}
return true;
}
async run() {
// Require ESLint v>=8.0.0
if (!this.isTest) {
this._checkEslint();
}
// To enable custom lint rules ('lint' or 'lint:dev')
if (this.lintType === "local") {
// Require local ESLint plugin installation and corresponding config
this._checkEslintPlugin();
await this._addEslintPluginConfig();
// Include custom lint sample rule and test ('lint:dev')
if (this.customRuleExample) {
// Generate custom lint rules dirs
await this._generateEslintDirs();
if (!exists(".eslint/rules/no-entity-moo.js")) {
await renderAndCopy(".eslint/rules", this.rulesPath);
}
if (!exists(".eslint/tests/no-entity-moo.test.js")) {
await renderAndCopy(".eslint/tests", this.testsPath);
}
}
// Generate VS Code extension settings
const { getFileExtensions } = require("@sap/eslint-plugin-cds/lib/api");
const ALLOWED_FILE_EXTENSIONS = getFileExtensions();
const extensions = new Set(ALLOWED_FILE_EXTENSIONS.map((ext) => {
return path.extname(ext).replace('.', '');
}))
extensions.forEach((ext) => {
if (ext in VSCODE_FILE_ASSOCIATIONS) {
VSCODE_FILE_ASSOCIATIONS[ext].forEach((assoc) => {
extensions.add(assoc)
})
}
});
await io.sanitizeVscodeSettings(
path.join(cds.root, ".vscode/settings.json"),
this.lintType,
Array.from(extensions),
this.customRuleExample
);
}
}
async finalize() {
if (this.lintType !== "global") {
if (this.missingNpmDependencies["names"].length > 0) {
const task = `npm install --save-dev ${this.missingNpmDependencies[
"names"
].join(" ")}`;
if (cds.cli.command === COMMAND_INIT) {
this._runTask(task + ' --package-lock-only');
return;
}
if (cds.cli.options.force) {
console.log(
`\n\nAlmost done - installing ${this.missingNpmDependencies["names"].length} npm dependencies:\n${task}`
);
this._runTask(task);
} else {
console.log(
`\n\nAlmost done - ${term.warn(
`you are missing ${this.missingNpmDependencies["names"].length} npm dependencies`
)}:\n`
);
this.missingNpmDependencies["msgs"].forEach((msg, i) => {
console.log(`(${i + 1}) ${msg}`);
if (i === this.missingNpmDependencies["msgs"].length - 1) {
console.log("\n");
}
});
await this._promptInstallDependencies(task);
}
}
}
}
_checkEslint() {
const eslintCmd = checks.resolveEslint(cds.root, '');
if (!eslintCmd) {
this.missingNpmDependencies["names"].push("eslint@^9");
this.missingNpmDependencies["msgs"].push("ESLint v>=9.0.0");
}
return;
}
_checkEslintPlugin() {
const eslintPluginLocal = checks.resolvePlugin(
"@sap/eslint-plugin-cds",
cds.root
);
// Require locally installed ESLint CDS plugin
if (!eslintPluginLocal) {
this.missingNpmDependencies["names"].push("@sap/eslint-plugin-cds@^3");
this.missingNpmDependencies["msgs"].push("ESLint plugin for CDS v>=3.0.0");
}
return;
}
async _generateEslintDirs() {
await mkdirp(this.docsPath);
await mkdirp(this.rulesPath);
await mkdirp(this.testsPath);
}
async _addEslintPluginConfig() {
const { getConfigPath } = require("@sap/eslint-plugin-cds/lib/api");
let configPath = getConfigPath(cds.root);
if (!configPath) {
configPath = path.join(cds.root, "eslint.config.mjs");
}
await io.sanitizeEslintConfig(configPath, this.customRuleExample);
}
async _promptInstallDependencies(task) {
if (!process.stdin.isTTY) {
return; // non-interactive shell, like in tests
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Do not install dependencies by default
let received = "no";
const timerID = setTimeout(() => {
rl.write("no" + os.EOL);
}, 10000);
// Ask user whether dependencies should be installed
const question = `${term.warn("Install dependencies now")} ? [y/n] `;
let todos = `${term.error(
"Aborted!\nPlease install the following components manually:"
)}\n\n${term.warn(task)}`;
return new Promise((resolve, reject) => {
rl.question(question, (answer) => {
rl.close();
clearTimeout(timerID);
received = answer.trim().toLowerCase();
// If user confirms or force flag is set, install dependencies
if (["y", "yes"].includes(received) || cds.cli.options.force) {
resolve(this._runTask(task));
// If use declines or question timeout is reached, print todos
} else if (["n", "no"].includes(received.toLowerCase())) {
console.log(todos);
resolve();
} else {
// Close and restart question (with new timeout)
rl.close();
clearTimeout(timerID);
this._promptInstallDependencies(task).then(resolve).catch(reject);
}
});
})
}
_runTask(task) {
let doneTask = true;
try {
cp.execSync(task, {
cwd: cds.root,
shell: IS_WIN,
stdio: "inherit",
});
} catch (err) {
doneTask = false;
}
return doneTask;
}
};