diff --git a/packages/xarc-app-dev/src/config/eslint/.eslintrc-mocha-test b/packages/xarc-app-dev/src/config/eslint/.eslintrc-mocha-test index 98dc3ab6e..74855813d 100644 --- a/packages/xarc-app-dev/src/config/eslint/.eslintrc-mocha-test +++ b/packages/xarc-app-dev/src/config/eslint/.eslintrc-mocha-test @@ -7,6 +7,7 @@ globals: sinon: false env: mocha: true + node: true rules: "no-unused-expressions": "off" # for `chai.expect` "max-len": ["error", 100, 2, {ignorePattern: "^\\s*(?:it|describe)\\(.*"}] diff --git a/packages/xarc-app-dev/src/config/eslint/.eslintrc-node b/packages/xarc-app-dev/src/config/eslint/.eslintrc-node index cfde8869f..df302e694 100644 --- a/packages/xarc-app-dev/src/config/eslint/.eslintrc-node +++ b/packages/xarc-app-dev/src/config/eslint/.eslintrc-node @@ -12,3 +12,6 @@ rules: "no-process-exit": ["off"] "func-style": ["off"] "no-multi-spaces": "off" +env: + node: true + diff --git a/packages/xarc-app-dev/src/config/eslint/.eslintrc-react b/packages/xarc-app-dev/src/config/eslint/.eslintrc-react index bbf0ba5d9..cbcaff629 100644 --- a/packages/xarc-app-dev/src/config/eslint/.eslintrc-react +++ b/packages/xarc-app-dev/src/config/eslint/.eslintrc-react @@ -8,6 +8,7 @@ globals: ReactElement: false env: mocha: true + browser: true plugins: - flowtype - react diff --git a/packages/xarc-app-dev/src/config/eslint/.eslintrc-react-test b/packages/xarc-app-dev/src/config/eslint/.eslintrc-react-test index c618b0ace..c7e51068c 100644 --- a/packages/xarc-app-dev/src/config/eslint/.eslintrc-react-test +++ b/packages/xarc-app-dev/src/config/eslint/.eslintrc-react-test @@ -9,6 +9,7 @@ globals: Element: false env: mocha: true + browser: true plugins: - flowtype - react diff --git a/packages/xarc-app-dev/src/lib/dev-tasks.ts b/packages/xarc-app-dev/src/lib/dev-tasks.ts index 6577cc2be..bdf51485e 100644 --- a/packages/xarc-app-dev/src/lib/dev-tasks.ts +++ b/packages/xarc-app-dev/src/lib/dev-tasks.ts @@ -24,6 +24,9 @@ const scanDir = require("filter-scan-dir"); const xsh = require("xsh"); const logger = require("./logger"); import { createGitIgnoreDir } from "./utils"; +import { jestTestDirectories } from "./tasks/constants"; +import { eslintTasks } from "./tasks/eslint"; + let xarcCwd: string; /** @@ -121,7 +124,6 @@ export function loadXarcDevTasks(xrun, userOptions: XarcOptions = {}) { const config = xarcOptions.config; const karmaConfig = file => Path.join(config.karma, file); const mochaConfig = file => Path.join(config.mocha, file); - const eslintConfig = file => Path.join(config.eslint, file); const shell = xsh.$; const exec = xsh.exec; @@ -130,8 +132,6 @@ export function loadXarcDevTasks(xrun, userOptions: XarcOptions = {}) { const penthouse = optionalRequire("penthouse"); const CleanCSS = optionalRequire("clean-css"); - const jestTestDirectories = ["_test_", "_tests_", "__test__", "__tests__"]; - const watchExec = (files, cmd) => { let timer; let child; @@ -192,64 +192,6 @@ export function loadXarcDevTasks(xrun, userOptions: XarcOptions = {}) { } catch (e) {} // eslint-disable-line } - /* - * There are multiple eslint config for different groups of code - * - * - eslintrc-react for directories client and templates (React Code) - * - eslintrc-react-test for test/client (React test code) - * - eslintrc-node for server (NodeJS code) - * - eslintrc-mocha-test for test/server and test/func (NodeJS test code) - * - * If the directory contains a .eslintrc then it's used instead - * - */ - - function lint(options) { - const ext = options.ext ? ` --ext ${options.ext}` : ""; - - const checkCustom = t => { - const f = ["", ".json", ".yml", ".yaml", ".js"].find(e => { - const x = Path.resolve(xarcCwd, Path.join(t, `.eslintrc${e}`)); - return Fs.existsSync(x); - }); - return f !== undefined; - }; - - // - // group target directories into custom and archetype - // custom - .eslintrc file exist - // archetype - no .eslintrc, use config from archetype - // - const grouped = options.targets.reduce( - (a, t) => { - (checkCustom(t) ? a.custom : a.archetype).push(t); - return a; - }, - { custom: [], archetype: [] } - ); - - const ignorePattern = options.ignorePatterns - ? options.ignorePatterns.map(p => `--ignore-pattern ${p}`) - : ""; - - const version = require("eslint/package.json") - .version.split(".") - .map(x => parseInt(x)); - const noUnmatchError = - version[0] > 6 && version[1] > 8 ? ` --no-error-on-unmatched-pattern` : ""; - - const commands = [ - grouped.custom.length > 0 && - `~$eslint${ext}${noUnmatchError} ${grouped.custom.join(" ")} ${ignorePattern}`, - grouped.archetype.length > 0 && - `~$eslint${ext}${noUnmatchError} --no-eslintrc -c ${ - options.config - } ${grouped.archetype.join(" ")} ${ignorePattern}` - ]; - - return Promise.resolve(commands.filter(x => x)); - } - /* * [generateServiceWorker clap task to generate service worker code that will precache specific * resources so they work offline.] @@ -1052,90 +994,7 @@ You only need to run this if you are doing something not through the xarc tasks. }); } - if (xarcOptions.options.eslint) { - const hasTest = Fs.existsSync("test"); - const hasTestServer = Fs.existsSync("test/server"); - // legacy src/client and src/server only setup? - let isLegacySrc = false; - try { - const files = Fs.readdirSync("src").filter(x => !x.startsWith(".")); - isLegacySrc = files.sort().join("") === "clientserver"; - } catch (err) { - // - } - - const lintTasks = [ - "lint-client", - hasTest && "lint-client-test", - "lint-server", - hasTestServer && "lint-server-test" - ].filter(x => x); - - Object.assign(tasks, { - lint: xclap2.concurrent(...lintTasks), - - "lint-client": { - desc: - "Run eslint on code in src/client and templates with react rules (ignore src/server)", - task: () => - lint({ - ext: ".js,.jsx,.ts,.tsx", - config: eslintConfig(".eslintrc-react"), - targets: isLegacySrc - ? [AppMode.src.client, "templates"] - : [AppMode.src.dir, "templates"], - ignorePatterns: [AppMode.src.server] - }) - }, - - "lint-server": { - desc: "Run eslint on server code in src/server with node.js rules", - task: () => - lint({ - ext: ".js,.jsx,.ts,.tsx", - config: eslintConfig(".eslintrc-node"), - targets: [AppMode.src.server] - }) - } - }); - - if (hasTest) { - tasks["lint-client-test"] = { - desc: "Run eslint on code in test with react rules (ignore test/server)", - task: () => - lint({ - ext: ".js,.jsx,.ts,.tsx", - config: eslintConfig(".eslintrc-react-test"), - targets: ["test", ...jestTestDirectories.map(dir => `${dir}`)], - ignorePatterns: ["test/server"] - }) - }; - } - - if (hasTestServer) { - tasks["lint-server-test"] = { - desc: "Run eslint on code in in test/server with node.js rules", - task: () => - lint({ - ext: ".js,.jsx,.ts,.tsx", - config: process.env.SERVER_ES6 - ? eslintConfig(".eslintrc-mocha-test-es6") - : eslintConfig(".eslintrc-mocha-test"), - targets: ["test/server"] - }) - }; - } - } else { - const lintDisabled = () => { - logger.info(`eslint tasks are disabled because @xarc/opt-eslint is not installed. - Please add it to your devDependencies to enable eslint.`); - }; - Object.assign(tasks, { - lint: lintDisabled, - "lint-server": lintDisabled, - "lint-server-test": lintDisabled - }); - } + Object.assign(tasks, eslintTasks(xarcOptions, xclap2)); if (xarcOptions.options.karma) { const noSingleRun = process.argv.indexOf("--no-single-run") >= 0 ? "--no-single-run" : ""; diff --git a/packages/xarc-app-dev/src/lib/index.ts b/packages/xarc-app-dev/src/lib/index.ts index 5c6085309..801a34336 100644 --- a/packages/xarc-app-dev/src/lib/index.ts +++ b/packages/xarc-app-dev/src/lib/index.ts @@ -1,4 +1,5 @@ import { loadXarcDevTasks } from "./dev-tasks"; +import * as Path from "path"; Object.defineProperties(loadXarcDevTasks, { require: { @@ -28,6 +29,30 @@ Object.defineProperties(loadXarcDevTasks, { return require("./webpack-dev-koa"); }, enumerable: false + }, + eslintReactTestRc: { + get() { + return Path.posix.join(__dirname, "../config/eslint/.eslintrc-react-test"); + }, + enumerable: false + }, + eslintReactRc: { + get() { + return Path.posix.join(__dirname, "../config/eslint/.eslintrc-react"); + }, + enumerable: false + }, + eslintNodeRc: { + get() { + return Path.posix.join(__dirname, "../config/eslint/.eslintrc-node"); + }, + enumerable: false + }, + eslintNodeTestRc: { + get() { + return Path.posix.join(__dirname, "../config/eslint/.eslintrc-mocha-test"); + }, + enumerable: false } }); diff --git a/packages/xarc-app-dev/src/lib/tasks/constants.ts b/packages/xarc-app-dev/src/lib/tasks/constants.ts new file mode 100644 index 000000000..ac6d0bffa --- /dev/null +++ b/packages/xarc-app-dev/src/lib/tasks/constants.ts @@ -0,0 +1,4 @@ +export const jestTestDirectories = ["_test_", "_tests_", "__test__", "__tests__"]; +export const appSourceDirs = ["src", "test"]; +export const allSourceDirs = [].concat(appSourceDirs, jestTestDirectories); +export const serverOnlySourceDirs = ["src/server", "src/server-routes", "test/server"]; diff --git a/packages/xarc-app-dev/src/lib/tasks/eslint.ts b/packages/xarc-app-dev/src/lib/tasks/eslint.ts new file mode 100644 index 000000000..a79a7c50e --- /dev/null +++ b/packages/xarc-app-dev/src/lib/tasks/eslint.ts @@ -0,0 +1,228 @@ +/* eslint-disable @typescript-eslint/no-var-requires, max-statements */ + +import * as Fs from "fs"; +import * as Path from "path"; +import { jestTestDirectories, allSourceDirs } from "./constants"; +const logger = require("../logger"); +const optionalRequire = require("optional-require")(require); + +/* + * There are multiple eslint config for different groups of code + * + * - eslintrc-react for directories client and templates (React Code) + * - eslintrc-react-test for test/client (React test code) + * - eslintrc-node for server (NodeJS code) + * - eslintrc-mocha-test for test/server and test/func (NodeJS test code) + * + * If the directory contains a .eslintrc then it's used instead + * + */ + +function lint(options, xarcOptions) { + const ext = options.ext ? ` --ext ${options.ext}` : ""; + + const checkCustom = t => { + const f = ["", ".json", ".yml", ".yaml", ".js"].find(e => { + const x = Path.resolve(xarcOptions.cwd, Path.join(t, `.eslintrc${e}`)); + return Fs.existsSync(x); + }); + return f !== undefined; + }; + + // + // group target directories into custom and archetype + // custom - .eslintrc file exist + // archetype - no .eslintrc, use config from archetype + // + const grouped = options.targets.reduce( + (a, t) => { + (checkCustom(t) ? a.custom : a.archetype).push(t); + return a; + }, + { custom: [], archetype: [] } + ); + + const ignorePattern = options.ignorePatterns + ? options.ignorePatterns.map(p => `--ignore-pattern ${p}`) + : ""; + + const version = require("eslint/package.json") + .version.split(".") + .map(x => parseInt(x)); + const noUnmatchError = version[0] > 6 && version[1] > 8 ? ` --no-error-on-unmatched-pattern` : ""; + + const commands = [ + grouped.custom.length > 0 && + `~$eslint${ext}${noUnmatchError} ${grouped.custom.join(" ")} ${ignorePattern}`, + grouped.archetype.length > 0 && + `~$eslint${ext}${noUnmatchError} -c ${options.config} ${grouped.archetype.join( + " " + )} ${ignorePattern}` + ]; + + return Promise.resolve(commands.filter(x => x)); +} + +/** + * return tasks to show eslint is not enabled + */ + +function eslintDisabledTasks() { + const lintDisabled = () => { + logger.info(`eslint tasks are disabled because @xarc/opt-eslint is not installed. + Please add it to your devDependencies to enable eslint.`); + }; + return { + lint: lintDisabled, + "lint-server": lintDisabled, + "lint-server-test": lintDisabled + }; +} + +/** + * Generate legacy tasks that were for eslint-4.0, messy, no JS config support etc + */ +export function eslint4Tasks(xarcOptions: any, xclap: any) { + const AppMode = xarcOptions.AppMode; + const tasks = {}; + + const config = xarcOptions.config; + const eslintConfig = file => Path.join(config.eslint, file); + + if (!xarcOptions.options.eslint) { + return eslintDisabledTasks(); + } + + const hasTest = Fs.existsSync("test"); + const hasTestServer = Fs.existsSync("test/server"); + // legacy src/client and src/server only setup? + let isLegacySrc = false; + try { + const files = Fs.readdirSync("src").filter(x => !x.startsWith(".")); + isLegacySrc = files.sort().join("") === "clientserver"; + } catch (err) { + // + } + + const lintTasks = [ + "lint-client", + hasTest && "lint-client-test", + "lint-server", + hasTestServer && "lint-server-test" + ].filter(x => x); + + Object.assign(tasks, { + lint: xclap.concurrent(...lintTasks), + + "lint-client": { + desc: "Run eslint on code in src/client and templates with react rules (ignore src/server)", + task: () => + lint( + { + ext: ".js,.jsx,.ts,.tsx", + config: eslintConfig(".eslintrc-react"), + targets: isLegacySrc + ? [AppMode.src.client, "templates"] + : [AppMode.src.dir, "templates"], + ignorePatterns: [AppMode.src.server] + }, + xarcOptions + ) + }, + + "lint-server": { + desc: "Run eslint on server code in src/server with node.js rules", + task: () => + lint( + { + ext: ".js,.jsx,.ts,.tsx", + config: eslintConfig(".eslintrc-node"), + targets: [AppMode.src.server] + }, + xarcOptions + ) + } + }); + + if (hasTest) { + tasks["lint-client-test"] = { + desc: "Run eslint on code in test with react rules (ignore test/server)", + task: () => + lint( + { + ext: ".js,.jsx,.ts,.tsx", + config: eslintConfig(".eslintrc-react-test"), + targets: ["test", ...jestTestDirectories.map(dir => `${dir}`)], + ignorePatterns: ["test/server"] + }, + xarcOptions + ) + }; + } + + if (hasTestServer) { + tasks["lint-server-test"] = { + desc: "Run eslint on code in in test/server with node.js rules", + task: () => + lint( + { + ext: ".js,.jsx,.ts,.tsx", + config: process.env.SERVER_ES6 + ? eslintConfig(".eslintrc-mocha-test-es6") + : eslintConfig(".eslintrc-mocha-test"), + targets: ["test/server"] + }, + xarcOptions + ) + }; + } + + return tasks; +} + +/** + * Generate tasks for eslint-7.0 + */ +export function eslint7Tasks(xarcOptions: any, xclap: any) { + if (!xarcOptions.options.eslint) { + return eslintDisabledTasks(); + } + + return { + lint: { + desc: `Run eslint for your sources - require setup .eslintrc.js`, + task: () => { + const validDirs = allSourceDirs + .map(d => { + const dir = Path.join(xarcOptions.cwd, d); + try { + const stat = Fs.statSync(dir); + if (stat.isDirectory()) { + return d; + } + } catch (err) { + // + } + return ""; + }) + .filter(x => x) + .join(" "); + return xclap.exec(`eslint ${validDirs}`); + } + } + }; +} + +export function eslintTasks(xarcOptions: any, xclap: any) { + // + const xarcOptPkg = optionalRequire("@xarc/opt-eslint/package.json"); + if (xarcOptPkg) { + const version = parseInt(xarcOptPkg.version.split(".")[0]); + + if (version >= 2) { + return eslint7Tasks(xarcOptions, xclap); + } + } + + return eslint4Tasks(xarcOptions, xclap); +}