diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab5afb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ca0e06a --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +"./prettier/index.mjs" diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..17b1024 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1 @@ +{ "extends": "./semantic-release/index.cjs" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..e746fd9 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# Style 🛼 + +> Roll in style. + +Highly opinionated configuration files for typescript projects. Inspired by [@vercel/style-guide](https://github.com/vercel/style-guide) + +## Usage + +```bash +npm i -D @timobechtel/style prettier eslint typescript +``` + +### Prettier + +```bash +echo '"@timobechtel/style/prettier/index.mjs"' > .prettierrc +``` + +### Typescript + +```bash +echo '{ "extends": "@timobechtel/style/tsconfig/core" }' > tsconfig.json +``` + +### Eslint + +```bash +echo 'const{resolve}=require("node:path");const project=resolve(process.cwd(),"tsconfig.json");module.exports={root:true,extends:[require.resolve("@timobechtel/style/eslint/core.cjs")],parserOptions:{project},settings:{"import/resolver":{typescript:{project}}}};' > .eslintrc.cjs +``` + +Or copy the following to a `.eslintrc.cjs` manually: + +```js +const { resolve } = require('node:path'); + +const project = resolve(process.cwd(), 'tsconfig.json'); + +module.exports = { + root: true, + extends: [require.resolve('@timobechtel/style/eslint/core.cjs')], + parserOptions: { + project, + }, + settings: { + 'import/resolver': { + typescript: { + project, + }, + }, + }, +}; +``` + +### semantic-release + +This repo also contains a [semantic-release](https://github.com/semantic-release/semantic-release) configuration. + +```bash +npm i -D semantic-release +``` + +```bash +echo '{ "extends": "@timobechtel/style/semantic-release/index.cjs" }' > .releaserc.json +``` diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ab24272 Binary files /dev/null and b/bun.lockb differ diff --git a/eslint/core.cjs b/eslint/core.cjs new file mode 100644 index 0000000..a8dd12a --- /dev/null +++ b/eslint/core.cjs @@ -0,0 +1,49 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + extends: [ + 'eslint:recommended', + 'plugin:import/recommended', + 'prettier', + require.resolve('./rules/base.cjs'), + require.resolve('./rules/import.cjs'), + require.resolve('./rules/unicorn.cjs'), + ], + plugins: [], + env: { + es2021: true, + node: true, + browser: true, + }, + // Report unused `eslint-disable` comments. + reportUnusedDisableDirectives: true, + // Tell ESLint not to ignore dot-files, which are ignored by default. + ignorePatterns: ['!.*.js'], + // Global settings used by all overrides. + settings: { + // Use the Node resolver by default. + 'import/resolver': { node: {} }, + }, + // Global parser options. + parserOptions: { + ecmaVersion: 2021, + sourceType: 'module', + }, + overrides: [ + { + files: ['*.ts?(x)'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', + 'plugin:@typescript-eslint/strict', + 'plugin:@typescript-eslint/strict-type-checked', + 'plugin:@typescript-eslint/stylistic', + 'plugin:@typescript-eslint/stylistic-type-checked', + 'plugin:import/typescript', + 'prettier', + require.resolve('./rules/typescript.cjs'), + ], + }, + ], +}); diff --git a/eslint/rules/base.cjs b/eslint/rules/base.cjs new file mode 100644 index 0000000..a8eaa8a --- /dev/null +++ b/eslint/rules/base.cjs @@ -0,0 +1,82 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + rules: { + // prefer arrow functions for callbacks + 'prefer-arrow-callback': [ + 'warn', + { + allowNamedFunctions: true, + allowUnboundThis: true, + }, + ], + // console logs other than error and warn are only needed for debugging, so they should be removed before merging + 'no-console': [ + 'error', + { + allow: [ + 'warn', + 'error', + 'clear', + // sometimes we really need to log something, even in production, so info is allowed in edge cases + 'info', + ], + }, + ], + 'max-depth': ['warn', 4], + // some globals, like "name" might be similar to the name of a variable, so we prevent them from being used + 'no-restricted-globals': ['error', 'event', 'name'], + // Require curly braces for multiline blocks. + curly: ['warn', 'multi-line'], + // Require default clauses in switch statements to be last (if used). + 'default-case-last': 'error', + // Require triple equals (`===` and `!==`). + eqeqeq: 'error', + // disallow the use of `alert()` + 'no-alert': 'error', + // Disallow renaming import, export, and destructured assignments to the same name. + 'no-useless-rename': 'warn', + // Require `let` or `const` instead of `var`. + 'no-var': 'error', + // Require object literal shorthand syntax. + 'object-shorthand': 'warn', + // Require default to `const` instead of `let`. + 'prefer-const': 'warn', + // Require rest parameters instead of `arguments`. + 'prefer-rest-params': 'error', + // Require spread syntax instead of `.apply()`. + 'prefer-spread': 'error', + // Require template literals instead of string concatenation. + 'prefer-template': 'warn', + // Disallow returning values from Promise executor functions. + 'no-promise-executor-return': 'error', + // Disallow loops with a body that allows only one iteration. + 'no-unreachable-loop': 'error', + // Require a capital letter for constructors. + 'new-cap': ['error', { capIsNew: false }], + // Disallow the omission of parentheses when invoking a constructor with no arguments. + 'new-parens': 'warn', + // Disallow if as the only statement in an else block. + 'no-lonely-if': 'warn', + // Disallow ternary operators when simpler alternatives exist. + 'no-unneeded-ternary': 'error', + // Require use of an object spread over Object.assign. + 'prefer-object-spread': 'warn', + // Disallow labels that share a name with a variable. + 'no-label-var': 'error', + // Disallow initializing variables to `undefined`. + 'no-undef-init': 'warn', + // Disallow unused variables. + 'no-unused-vars': [ + 'error', + { + args: 'after-used', + argsIgnorePattern: '^_', + ignoreRestSiblings: false, + vars: 'all', + varsIgnorePattern: '^_', + }, + ], + }, +}); diff --git a/eslint/rules/css-in-js.cjs b/eslint/rules/css-in-js.cjs new file mode 100644 index 0000000..8b6d5ac --- /dev/null +++ b/eslint/rules/css-in-js.cjs @@ -0,0 +1,16 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + rules: { + 'no-restricted-syntax': [ + 'error', + { + // catches common mistakes when writing comments in CSS + selector: + 'TemplateElement[value.cooked=/\\s+\\u002F\\u002F\\s*/], Literal[value=/\\s+\\u002F\\u002F\\s*/]', + message: 'Invalid comment syntax. Use `/* */` instead of `//`.', + }, + ], + }, +}); diff --git a/eslint/rules/import.cjs b/eslint/rules/import.cjs new file mode 100644 index 0000000..a6061ee --- /dev/null +++ b/eslint/rules/import.cjs @@ -0,0 +1,31 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + rules: { + // Disallow non-import statements appearing before import statements. + 'import/first': 'error', + // Require a newline after the last import/require. + 'import/newline-after-import': 'warn', + // Disallow a module from importing itself. + 'import/no-self-import': 'error', + // Ensures that there are no useless path segments. + 'import/no-useless-path-segments': ['error'], + // Enforce a module import order convention. + 'import/order': [ + 'warn', + { + groups: [ + 'builtin', // Node.js built-in modules + 'external', // Packages + 'internal', // Aliased modules + 'parent', // Relative parent + 'sibling', // Relative sibling + 'index', // Relative index + ], + }, + ], + // allow default exports + 'import/no-default-export': 'off', + }, +}); diff --git a/eslint/rules/typescript.cjs b/eslint/rules/typescript.cjs new file mode 100644 index 0000000..43fdcf5 --- /dev/null +++ b/eslint/rules/typescript.cjs @@ -0,0 +1,83 @@ +const { defineConfig } = require('eslint-define-config'); +const noUnusedVarsConfig = require('./base.cjs').rules['no-unused-vars']; + +// @ts-check +module.exports = defineConfig({ + rules: { + // Require consistent usage of type exports. + '@typescript-eslint/consistent-type-exports': [ + 'error', + { fixMixedExportsWithInlineTypeSpecifier: true }, + ], + // Require consistent usage of type imports. + '@typescript-eslint/consistent-type-imports': [ + 'error', + { + prefer: 'type-imports', + fixStyle: 'inline-type-imports', + }, + ], + /** + * Require using function property types in method signatures. + * These have enhanced typechecking, whereas method signatures do not. + */ + '@typescript-eslint/method-signature-style': 'error', + /** + * Require consistent naming conventions. + * Improves IntelliSense suggestions and avoids name collisions. + */ + '@typescript-eslint/naming-convention': [ + 'error', + // Anything type-like should be written in PascalCase. + { + format: ['PascalCase'], + selector: ['typeLike', 'enumMember'], + }, + // Interfaces cannot be prefixed with `I`, or have restricted names. + { + custom: { + match: false, + regex: '^I[A-Z]|^(Interface|Props|State)$', + }, + format: ['PascalCase'], + selector: 'interface', + }, + ], + // Disallow members of unions and intersections that do nothing or override type information. + '@typescript-eslint/no-redundant-type-constituents': 'error', + // Require using `RegExp.exec()` over `String.match()` for consistency. + '@typescript-eslint/prefer-regexp-exec': 'warn', + // + '@typescript-eslint/require-array-sort-compare': [ + 'error', + { ignoreStringArrays: true }, + ], + /** + * Require exhaustive checks when using union types in switch statements. + * This ensures cases are considered when items are later added to a union. + */ + '@typescript-eslint/switch-exhaustiveness-check': 'error', + // Require default parameters to be last. + 'default-param-last': 'off', + '@typescript-eslint/default-param-last': 'error', + // Disallow creation of functions within loops. + 'no-loop-func': 'off', + '@typescript-eslint/no-loop-func': 'error', + // Disallow unused variables. + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': noUnusedVarsConfig, + + /** + * These are enabled by `import/recommended`, but are better handled by + * TypeScript and @typescript-eslint. + */ + 'import/default': 'off', + 'import/export': 'off', + 'import/namespace': 'off', + 'import/no-unresolved': 'off', + + // This is disabled as we feel that checking empty strings is a valid use + // of `||` over `??`. + '@typescript-eslint/prefer-nullish-coalescing': 'off', + }, +}); diff --git a/eslint/rules/unicorn.cjs b/eslint/rules/unicorn.cjs new file mode 100644 index 0000000..61721a9 --- /dev/null +++ b/eslint/rules/unicorn.cjs @@ -0,0 +1,129 @@ +// @ts-check +const { defineConfig } = require('eslint-define-config'); + +module.exports = defineConfig({ + plugins: ['unicorn'], + rules: { + // Use destructured variables over properties. + 'unicorn/consistent-destructuring': 'warn', + // Move function definitions to the highest possible scope. + 'unicorn/consistent-function-scoping': [ + 'warn', + { + checkArrowFunctions: false, + }, + ], + // Enforce passing a message value when creating a built-in error. + 'unicorn/error-message': 'error', + // Require escape sequences to use uppercase values. + 'unicorn/escape-case': 'warn', + // Disallow empty files. + 'unicorn/no-empty-file': 'error', + // Do not use a for loop that can be replaced with a for-of loop. + 'unicorn/no-for-loop': 'warn', + // Require Array.isArray() instead of instanceof Array. + 'unicorn/no-instanceof-array': 'error', + // Prevent calling EventTarget#removeEventListener() with the result of an expression. + 'unicorn/no-invalid-remove-event-listener': 'error', + // Disallow the use of objects as default parameters. + 'unicorn/no-object-as-default-parameter': 'error', + // Disallow then property. + 'unicorn/no-thenable': 'error', + // Disallow awaiting non-promise values. + 'unicorn/no-unnecessary-await': 'error', + // Disallow unreadable IIFEs. + 'unicorn/no-unreadable-iife': 'warn', + // Disallow useless fallback when spreading in object literals. + 'unicorn/no-useless-fallback-in-spread': 'warn', + // Disallow useless array length check. + 'unicorn/no-useless-length-check': 'warn', + // Disallow returning/yielding Promise.resolve/reject() in async functions or promise callbacks + 'unicorn/no-useless-promise-resolve-reject': 'error', + // Disallow unnecessary spread. + 'unicorn/no-useless-spread': 'error', + // Disallow number literals with zero fractions or dangling dots. + 'unicorn/no-zero-fractions': 'warn', + // Enforce the style of numeric separators by correctly grouping digits. + 'unicorn/numeric-separators-style': 'error', + // Prefer .addEventListener() and .removeEventListener() over on-functions. + 'unicorn/prefer-add-event-listener': 'error', + // Prefer .find(…) and .findLast(…) over the first or last element from .filter(…). + 'unicorn/prefer-array-find': 'error', + // Prefer Array#flat() over legacy techniques to flatten arrays. + 'unicorn/prefer-array-flat': 'error', + // Prefer .flatMap(…) over .map(…).flat(). + 'unicorn/prefer-array-flat-map': 'error', + // Prefer Array#{indexOf,lastIndexOf}() over Array#{findIndex,findLastIndex}() when looking for the index of an item. + 'unicorn/prefer-array-index-of': 'error', + // Prefer .some(…) over .filter(…).length check and .{find,findLast}(…). + 'unicorn/prefer-array-some': 'error', + // Prefer .at() method for index access and String#charAt(). + 'unicorn/prefer-at': 'error', + // Prefer Blob#arrayBuffer() over FileReader#readAsArrayBuffer(…) and Blob#text() over FileReader#readAsText(…). + 'unicorn/prefer-blob-reading-methods': 'error', + // Prefer Date.now() to get the number of milliseconds since the Unix Epoch. + 'unicorn/prefer-date-now': 'error', + // Prefer default parameters over reassignment. + 'unicorn/prefer-default-parameters': 'warn', + // Prefer Node#append() over Node#appendChild(). + 'unicorn/prefer-dom-node-append': 'error', + // Prefer using .dataset on DOM elements over calling attribute methods. + 'unicorn/prefer-dom-node-dataset': 'error', + // Prefer childNode.remove() over parentNode.removeChild(childNode). + 'unicorn/prefer-dom-node-remove': 'error', + // Prefer EventTarget over EventEmitter. + 'unicorn/prefer-event-target': 'warn', + // Prefer export…from when re-exporting. + 'unicorn/prefer-export-from': [ + 'warn', + { + ignoreUsedVariables: true, + }, + ], + // Prefer .includes() over .indexOf() and Array#some() when checking for existence or non-existence. + 'unicorn/prefer-includes': 'error', + // Prefer KeyboardEvent#key over KeyboardEvent#keyCode. + 'unicorn/prefer-keyboard-event-key': 'error', + // Prefer using a logical operator over a ternary. + 'unicorn/prefer-logical-operator-over-ternary': 'error', + // Enforce the use of Math.trunc instead of bitwise operators. + 'unicorn/prefer-math-trunc': 'error', + // Prefer .before() over .insertBefore(), .replaceWith() over .replaceChild(), prefer one of .before(), .after(), .append() or .prepend() over insertAdjacentText() and insertAdjacentElement(). + 'unicorn/prefer-modern-dom-apis': 'error', + // Prefer modern Math APIs over legacy patterns. + 'unicorn/prefer-modern-math-apis': 'error', + // Prefer negative index over .length - index when possible. + 'unicorn/prefer-negative-index': 'error', + // Require using the `node:` protocol when importing Node.js built-in modules. + 'unicorn/prefer-node-protocol': 'error', + // Prefer Number static properties over global ones. + 'unicorn/prefer-number-properties': ['error', { checkInfinity: false }], + // Prefer using Object.fromEntries(…) to transform a list of key-value pairs into an object. + 'unicorn/prefer-object-from-entries': 'error', + // Prefer Reflect.apply() over Function#apply(). + 'unicorn/prefer-reflect-apply': 'error', + // using RegExp.test() is faster than string.match() + // note: you should not use the global flag /g with RegExp.test() though! + 'unicorn/prefer-regexp-test': 'error', + // Prefer Set#has() over Array#includes() when checking for existence or non-existence. + 'unicorn/prefer-set-has': 'error', + // Prefer using Set#size instead of Array#length. + 'unicorn/prefer-set-size': 'error', + // Prefer the spread operator over Array.from(). + 'unicorn/prefer-spread': 'error', + // Prefer String#replaceAll() over regex searches with the global flag. + 'unicorn/prefer-string-replace-all': 'error', + // Prefer String#slice() over String#substr() and String#substring(). + 'unicorn/prefer-string-slice': 'error', + // Prefer String#startsWith() & String#endsWith() over RegExp#test(). + 'unicorn/prefer-string-starts-ends-with': 'error', + // Prefer ternary expressions over simple if-else statements. + 'unicorn/prefer-ternary': ['warn', 'only-single-line'], + // Prefer top-level await over top-level promises and async function calls. + 'unicorn/prefer-top-level-await': 'error', + // Enforce using the separator argument with Array#join(). + 'unicorn/require-array-join-separator': 'error', + // Enforce consistent brace style for case clauses. + 'unicorn/switch-case-braces': 'error', + }, +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..06d6889 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "@timobechtel/style", + "version": "0.0.0", + "type": "module", + "files": [ + "bin", + "eslint", + "prettier", + "semantic-release", + "tsconfig" + ], + "devDependencies": { + "eslint": "^8.53.0", + "prettier": "^3.0.3", + "semantic-release": "^22.0.7", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "eslint": "^8.53.0", + "prettier": "^3.0.3", + "semantic-release": "^22.0.7", + "typescript": "^5.2.2" + }, + "dependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "eslint-config-prettier": "^9.0.0", + "eslint-define-config": "^1.24.1", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-unicorn": "^49.0.0" + } +} diff --git a/prettier/index.mjs b/prettier/index.mjs new file mode 100644 index 0000000..0df0ef9 --- /dev/null +++ b/prettier/index.mjs @@ -0,0 +1,18 @@ +/** + * Some of Prettier's defaults can be overridden by an EditorConfig file. We + * define those here to ensure that doesn't happen. + * + * See: https://github.com/prettier/prettier/blob/main/docs/configuration.md#editorconfig + */ +const overridableDefaults = { + endOfLine: 'lf', + tabWidth: 2, + printWidth: 80, + useTabs: false, +}; + +/** @type {import("prettier").Config} */ +export default { + ...overridableDefaults, + singleQuote: true, +}; diff --git a/semantic-release/index.cjs b/semantic-release/index.cjs new file mode 100644 index 0000000..9bb2d8f --- /dev/null +++ b/semantic-release/index.cjs @@ -0,0 +1,21 @@ +/** + * @type {import('semantic-release').GlobalConfig} + */ +module.exports = { + branches: [ + 'main', + { + name: 'canary', + channel: 'canary', + prerelease: true, + }, + ], + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + '@semantic-release/changelog', + '@semantic-release/npm', + '@semantic-release/github', + '@semantic-release/git', + ], +}; diff --git a/tsconfig/core.json b/tsconfig/core.json new file mode 100644 index 0000000..1cbe034 --- /dev/null +++ b/tsconfig/core.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": true, + "incremental": true, + "resolveJsonModule": true + }, + "exclude": ["node_modules"] +}