import { join } from '@angular-devkit/core'; import { chain, Rule, Tree, SchematicContext, } from '@angular-devkit/schematics'; import { addDepsToPackageJson } from './ast-utils'; import { eslintVersion, typescriptESLintVersion, eslintConfigPrettierVersion, nxVersion, tslintVersion, buildAngularVersion, } from './versions'; import { offsetFromRoot } from '@nrwl/devkit'; export const enum Linter { TsLint = 'tslint', EsLint = 'eslint', None = 'none', } export function generateProjectLint( projectRoot: string, tsConfigPath: string, linter: Linter, eslintFilePatterns: string[] ) { if (linter === Linter.TsLint) { return { builder: '@angular-devkit/build-angular:tslint', options: { tsConfig: [tsConfigPath], exclude: ['**/node_modules/**', `!${projectRoot}/**/*`], }, }; } else if (linter === Linter.EsLint) { return { builder: '@nrwl/linter:eslint', options: { lintFilePatterns: eslintFilePatterns, }, }; } else { return undefined; } } interface AddLintFileOptions { onlyGlobal?: boolean; localConfig?: any; extraPackageDeps?: { dependencies: { [key: string]: string }; devDependencies: { [key: string]: string }; }; } export function addLintFiles( projectRoot: string, linter: Linter, options: AddLintFileOptions = {} ): Rule { return (host: Tree, context: SchematicContext) => { if (options.onlyGlobal && options.localConfig) { throw new Error( 'onlyGlobal and localConfig cannot be used at the same time' ); } const chainedCommands = []; if (linter === 'tslint') { chainedCommands.push((host: Tree) => { if (!host.exists('/tslint.json')) { host.create('/tslint.json', globalTsLint); } if (!options.onlyGlobal) { host.create( join(projectRoot as any, `tslint.json`), JSON.stringify({ extends: `${offsetFromRoot(projectRoot)}tslint.json`, // Include project files to be linted since the global one excludes all files. linterOptions: { exclude: ['!**/*'], }, rules: {}, }) ); } }); chainedCommands.push( addDepsToPackageJson( {}, { tslint: tslintVersion, '@angular-devkit/build-angular': buildAngularVersion, } ) ); return chain(chainedCommands); } if (linter === 'eslint') { if (!host.exists('/.eslintrc.json')) { host.create('/.eslintrc.json', globalESLint); } chainedCommands.push( addDepsToPackageJson( { ...(options.extraPackageDeps ? options.extraPackageDeps.dependencies : {}), }, { '@nrwl/linter': nxVersion, '@nrwl/eslint-plugin-nx': nxVersion, '@typescript-eslint/parser': typescriptESLintVersion, '@typescript-eslint/eslint-plugin': typescriptESLintVersion, eslint: eslintVersion, 'eslint-config-prettier': eslintConfigPrettierVersion, ...(options.extraPackageDeps?.devDependencies ?? {}), } ) ); if (!options.onlyGlobal) { chainedCommands.push((host: Tree) => { let configJson; const rootConfig = `${offsetFromRoot(projectRoot)}.eslintrc.json`; // Include all project files to be linted (since they are turned off in the root eslintrc file). const ignorePatterns = ['!**/*']; if (options.localConfig) { /** * The end config is much easier to reason about if "extends" comes first, * so as well as applying the extension from the root lint config, we also * adjust the config to make extends come first. */ const { extends: extendsVal, ...localConfigExceptExtends } = options.localConfig; const extendsOption = extendsVal ? Array.isArray(extendsVal) ? extendsVal : [extendsVal] : []; configJson = { extends: [...extendsOption, rootConfig], ignorePatterns, ...localConfigExceptExtends, }; } else { configJson = { extends: rootConfig, ignorePatterns, overrides: [ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], parserOptions: { /** * In order to ensure maximum efficiency when typescript-eslint generates TypeScript Programs * behind the scenes during lint runs, we need to make sure the project is configured to use its * own specific tsconfigs, and not fall back to the ones in the root of the workspace. */ project: [`${projectRoot}/tsconfig.*?.json`], }, /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to */ rules: {}, }, { files: ['*.ts', '*.tsx'], rules: {}, }, { files: ['*.js', '*.jsx'], rules: {}, }, ], }; } host.create( join(projectRoot as any, `.eslintrc.json`), JSON.stringify(configJson) ); }); } return chain(chainedCommands); } }; } const globalTsLint = ` { "rulesDirectory": ["node_modules/@nrwl/workspace/src/tslint"], "linterOptions": { "exclude": ["**/*"] }, "rules": { "arrow-return-shorthand": true, "callable-types": true, "class-name": true, "deprecation": { "severity": "warn" }, "forin": true, "import-blacklist": [true, "rxjs/Rx"], "interface-over-type-literal": true, "member-access": false, "member-ordering": [ true, { "order": [ "static-field", "instance-field", "static-method", "instance-method" ] } ], "no-arg": true, "no-bitwise": true, "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], "no-construct": true, "no-debugger": true, "no-duplicate-super": true, "no-empty": false, "no-empty-interface": true, "no-eval": true, "no-inferrable-types": [true, "ignore-params"], "no-misused-new": true, "no-non-null-assertion": true, "no-shadowed-variable": true, "no-string-literal": false, "no-string-throw": true, "no-switch-case-fall-through": true, "no-unnecessary-initializer": true, "no-unused-expression": true, "no-var-keyword": true, "object-literal-sort-keys": false, "prefer-const": true, "radix": true, "triple-equals": [true, "allow-null-check"], "unified-signatures": true, "variable-name": false, "nx-enforce-module-boundaries": [ true, { "enforceBuildableLibDependency": true, "allow": [], "depConstraints": [ { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } ] } ] } } `; const globalESLint = JSON.stringify({ root: true, ignorePatterns: ['**/*'], plugins: ['@nrwl/nx'], /** * We leverage ESLint's "overrides" capability so that we can set up a root config which will support * all permutations of Nx workspaces across all frameworks, libraries and tools. * * The key point is that we need entirely different ESLint config to apply to different types of files, * but we still want to share common config where possible. */ overrides: [ /** * This configuration is intended to apply to all "source code" (but not * markup like HTML, or other custom file types like GraphQL) */ { files: ['*.ts', '*.tsx', '*.js', '*.jsx'], rules: { '@nrwl/nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: [], depConstraints: [ { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, ], }, ], }, }, /** * This configuration is intended to apply to all TypeScript source files. * See the eslint-plugin-nx package for what is in the referenced shareable config. */ { files: ['*.ts', '*.tsx'], extends: ['plugin:@nrwl/nx/typescript'], /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to */ rules: {}, }, /** * This configuration is intended to apply to all JavaScript source files. * See the eslint-plugin-nx package for what is in the referenced shareable config. */ { files: ['*.js', '*.jsx'], extends: ['plugin:@nrwl/nx/javascript'], /** * Having an empty rules object present makes it more obvious to the user where they would * extend things from if they needed to */ rules: {}, }, ], });