Basic Architecture of Sport Web
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
|
||||
import importX from 'eslint-plugin-import-x';
|
||||
import turbo from 'eslint-plugin-turbo';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
/**
|
||||
* Base flat config: TypeScript rules + import hygiene + the architectural
|
||||
* boundary rules that every package in the workspace must obey.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
*/
|
||||
export const baseConfig = [
|
||||
{
|
||||
ignores: [
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/.next/**',
|
||||
'**/.turbo/**',
|
||||
'**/coverage/**',
|
||||
'**/node_modules/**',
|
||||
'**/generated/**',
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
importX.flatConfigs.recommended,
|
||||
importX.flatConfigs.typescript,
|
||||
turbo.configs['flat/recommended'],
|
||||
{
|
||||
languageOptions: {
|
||||
ecmaVersion: 2023,
|
||||
globals: { ...globals.node, ...globals.es2021 },
|
||||
},
|
||||
settings: {
|
||||
// TypeScript resolver first: it understands `.ts` extensionless imports,
|
||||
// path aliases and workspace `exports` maps. The node resolver is the
|
||||
// fallback for plain JS config files.
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({ alwaysTryTypes: true, project: ['*/tsconfig.json'] }),
|
||||
importX.createNodeResolver(),
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
// --- Type safety -------------------------------------------------
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
// `import type` statements stay with their source group rather than being
|
||||
// herded to the bottom of the file, where they lose the context of what
|
||||
// they belong to.
|
||||
'@typescript-eslint/no-import-type-side-effects': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
|
||||
// --- Import hygiene ----------------------------------------------
|
||||
'import-x/no-default-export': 'error',
|
||||
'import-x/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
pathGroups: [{ pattern: '@sport/**', group: 'internal', position: 'before' }],
|
||||
pathGroupsExcludedImportTypes: ['type'],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'import-x/no-cycle': ['error', { maxDepth: 4 }],
|
||||
|
||||
// --- General ------------------------------------------------------
|
||||
eqeqeq: ['error', 'smart'],
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'prefer-const': 'error',
|
||||
'object-shorthand': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
// Config files and scripts are allowed default exports and console output.
|
||||
files: ['**/*.config.{js,mjs,ts}', '**/scripts/**', '**/*.cjs'],
|
||||
rules: {
|
||||
'import-x/no-default-export': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{test,spec}.{ts,tsx}', '**/test/**'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
prettier,
|
||||
];
|
||||
|
||||
export default baseConfig;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { baseConfig } from './base.js';
|
||||
|
||||
/**
|
||||
* NestJS config.
|
||||
*
|
||||
* The two rules that matter architecturally:
|
||||
* 1. Only the persistence layer may import PrismaService / Redis directly.
|
||||
* 2. A module must not deep-import another module's internals — cross-module
|
||||
* access goes through the other module's public surface (`<module>/public`).
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
*/
|
||||
export const nestConfig = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
/**
|
||||
* OFF, and it must stay off.
|
||||
*
|
||||
* Nest resolves constructor dependencies from `emitDecoratorMetadata`,
|
||||
* which requires the parameter's type to survive as a *value* import.
|
||||
* This rule cannot tell an injected class from a pure type, so its
|
||||
* autofix silently rewrites `import { PrismaService }` into
|
||||
* `import { type PrismaService }` — which compiles cleanly and then
|
||||
* fails at runtime with "Nest can't resolve dependencies".
|
||||
*
|
||||
* The same reasoning is why `verbatimModuleSyntax` is disabled in
|
||||
* @sport/config/typescript/nestjs.json.
|
||||
*/
|
||||
'@typescript-eslint/consistent-type-imports': 'off',
|
||||
|
||||
// Nest relies on parameter decorators and class-based DI.
|
||||
'@typescript-eslint/no-extraneous-class': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'no-console': 'error',
|
||||
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['@/modules/*/*', '!@/modules/*/public'],
|
||||
message:
|
||||
'Cross-module deep imports are forbidden. Import from `@/modules/<name>/public` instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Decorated DTO / entity classes legitimately have empty bodies.
|
||||
files: ['**/*.dto.ts', '**/*.entity.ts'],
|
||||
rules: { '@typescript-eslint/no-extraneous-class': 'off' },
|
||||
},
|
||||
];
|
||||
|
||||
export default nestConfig;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { reactConfig } from './react.js';
|
||||
|
||||
/**
|
||||
* Next.js App Router config.
|
||||
*
|
||||
* Note: the App Router *requires* default exports for `page`/`layout`/`route`
|
||||
* and friends, so the workspace-wide `no-default-export` rule is relaxed for
|
||||
* those files only — everywhere else named exports remain mandatory.
|
||||
*
|
||||
* @type {import("eslint").Linter.Config[]}
|
||||
*/
|
||||
export const nextConfig = [
|
||||
...reactConfig,
|
||||
{
|
||||
files: [
|
||||
'src/app/**/{page,layout,template,loading,error,not-found,default,route,global-error,sitemap,robots,opengraph-image,icon,apple-icon,manifest}.{ts,tsx}',
|
||||
'src/middleware.ts',
|
||||
'next.config.{ts,mjs,js}',
|
||||
'instrumentation.ts',
|
||||
],
|
||||
rules: {
|
||||
'import-x/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
rules: {
|
||||
// The storefront and admin must reach the backend only through the
|
||||
// generated API client — never with ad-hoc fetch calls or a DB driver.
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@prisma/client',
|
||||
message:
|
||||
'Frontend apps must never talk to the database. Use @sport/api-client instead.',
|
||||
},
|
||||
{
|
||||
name: 'ioredis',
|
||||
message: 'Frontend apps must never talk to Redis. Use @sport/api-client instead.',
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['@sport/api/*', '@sport/api'],
|
||||
message: 'Frontend apps must not import backend code. Use @sport/api-client.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@sport/eslint-config",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Shared ESLint flat configs for the workspace.",
|
||||
"exports": {
|
||||
"./base": "./base.js",
|
||||
"./react": "./react.js",
|
||||
"./next": "./next.js",
|
||||
"./nest": "./nest.js"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "echo 'no lint target'",
|
||||
"typecheck": "echo 'no typecheck target'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "^9.39.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.5",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-turbo": "^2.10.9",
|
||||
"globals": "^16.5.0",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import globals from 'globals';
|
||||
import a11y from 'eslint-plugin-jsx-a11y';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
import { baseConfig } from './base.js';
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
export const reactConfig = [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
...react.configs.flat.recommended,
|
||||
languageOptions: {
|
||||
...react.configs.flat.recommended.languageOptions,
|
||||
globals: { ...globals.browser, ...globals.serviceworker },
|
||||
},
|
||||
settings: { react: { version: 'detect' } },
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: { 'react-hooks': reactHooks, 'jsx-a11y': a11y },
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
...a11y.flatConfigs.recommended.rules,
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never' }],
|
||||
'react/self-closing-comp': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default reactConfig;
|
||||
Reference in New Issue
Block a user