@synergyerp/backend-standards 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/PULL_REQUEST_TEMPLATE.md +52 -0
- package/.github/workflows/ci-template.yml +49 -0
- package/.github/workflows/ci.yml +43 -0
- package/.github/workflows/publish.yml +36 -0
- package/.husky/commit-msg +5 -0
- package/.husky/pre-commit +5 -0
- package/.husky/pre-push +5 -0
- package/.prettierignore +8 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +28 -0
- package/README.md +103 -0
- package/commitlint.config.js +33 -0
- package/eslint.config.js +189 -0
- package/package.json +76 -0
- package/prettier.config.js +12 -0
- package/scripts/check-modularization.mjs +109 -0
- package/tsconfig.base.json +27 -0
- package/vitest.config.base.ts +33 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
|
|
3
|
+
<!-- Provide a clear and concise description of the change. -->
|
|
4
|
+
|
|
5
|
+
## Type of Change
|
|
6
|
+
|
|
7
|
+
- [ ] feat: New feature
|
|
8
|
+
- [ ] fix: Bug fix
|
|
9
|
+
- [ ] security: Security-related fix
|
|
10
|
+
- [ ] perf: Performance improvement
|
|
11
|
+
- [ ] refactor: Code restructure
|
|
12
|
+
- [ ] test: Test additions/updates
|
|
13
|
+
- [ ] docs: Documentation
|
|
14
|
+
- [ ] chore: Build/tooling/dependencies
|
|
15
|
+
- [ ] ci: CI/CD changes
|
|
16
|
+
|
|
17
|
+
## Breaking Change?
|
|
18
|
+
|
|
19
|
+
- [ ] Yes (describe below)
|
|
20
|
+
- [ ] No
|
|
21
|
+
|
|
22
|
+
## Testing Done
|
|
23
|
+
|
|
24
|
+
- [ ] Unit tests added/updated
|
|
25
|
+
- [ ] Integration tests added/updated
|
|
26
|
+
- [ ] Manual testing performed
|
|
27
|
+
- [ ] No testing required (explain why)
|
|
28
|
+
|
|
29
|
+
## Module Isolation Compliance
|
|
30
|
+
|
|
31
|
+
- [ ] New module has an `index.ts` barrel file
|
|
32
|
+
- [ ] Barrel only exports the service (NOT repository, controller, or validation)
|
|
33
|
+
- [ ] No cross-module deep imports (all cross-module access goes through barrel → service)
|
|
34
|
+
- [ ] No flat files at `src/modules/` root
|
|
35
|
+
- [ ] `pnpm check-modularization` passes
|
|
36
|
+
|
|
37
|
+
## Checklist
|
|
38
|
+
|
|
39
|
+
- [ ] Lint passes (`pnpm lint`)
|
|
40
|
+
- [ ] Type check passes (`pnpm check-types`)
|
|
41
|
+
- [ ] Tests pass (`pnpm test:ci`)
|
|
42
|
+
- [ ] Coverage thresholds met (≥80% lines, ≥75% branches)
|
|
43
|
+
- [ ] No hardcoded secrets, credentials, or API keys
|
|
44
|
+
- [ ] No `console.log` or `debugger` statements
|
|
45
|
+
- [ ] PR title follows conventional commit format
|
|
46
|
+
- [ ] Branch up to date with `develop`
|
|
47
|
+
- [ ] Database migrations are backward-compatible (if applicable)
|
|
48
|
+
- [ ] Security implications considered
|
|
49
|
+
|
|
50
|
+
## Related Issues
|
|
51
|
+
|
|
52
|
+
<!-- Link related issues. Use "Closes #123" syntax. -->
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI Pipeline (Template)
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
concurrency:
|
|
7
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
8
|
+
cancel-in-progress: true
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
NODE_VERSION: '22'
|
|
12
|
+
PNPM_VERSION: '11'
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
quality:
|
|
16
|
+
name: Quality Checks
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 15
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
with:
|
|
22
|
+
fetch-depth: 0
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
26
|
+
- uses: pnpm/action-setup@v2
|
|
27
|
+
with:
|
|
28
|
+
version: ${{ env.PNPM_VERSION }}
|
|
29
|
+
- name: Cache pnpm
|
|
30
|
+
uses: actions/cache@v4
|
|
31
|
+
with:
|
|
32
|
+
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
33
|
+
path: ~/.pnpm-store
|
|
34
|
+
- run: pnpm install --frozen-lockfile
|
|
35
|
+
- run: pnpm lint
|
|
36
|
+
- run: pnpm check-modularization
|
|
37
|
+
- name: Validate Commit Messages
|
|
38
|
+
if: github.event_name == 'pull_request'
|
|
39
|
+
run: pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
|
|
40
|
+
- run: pnpm check-types
|
|
41
|
+
- run: pnpm format:check
|
|
42
|
+
- run: pnpm test:ci
|
|
43
|
+
- run: pnpm audit --audit-level=high
|
|
44
|
+
- run: pnpm build
|
|
45
|
+
- uses: codecov/codecov-action@v4
|
|
46
|
+
if: always()
|
|
47
|
+
with:
|
|
48
|
+
files: ./coverage/cobertura-coverage.xml
|
|
49
|
+
fail_ci_if_error: false
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
NODE_VERSION: '22'
|
|
15
|
+
PNPM_VERSION: '11'
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
validate:
|
|
19
|
+
name: Validate Standards Package
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 10
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
28
|
+
|
|
29
|
+
- uses: pnpm/action-setup@v2
|
|
30
|
+
with:
|
|
31
|
+
version: ${{ env.PNPM_VERSION }}
|
|
32
|
+
|
|
33
|
+
- name: Install
|
|
34
|
+
run: pnpm install --frozen-lockfile
|
|
35
|
+
|
|
36
|
+
- name: Lint
|
|
37
|
+
run: pnpm lint
|
|
38
|
+
|
|
39
|
+
- name: Format check
|
|
40
|
+
run: pnpm format:check
|
|
41
|
+
|
|
42
|
+
- name: Audit
|
|
43
|
+
run: pnpm audit --audit-level=high
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to GitHub Packages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
env:
|
|
10
|
+
NODE_VERSION: '22'
|
|
11
|
+
PNPM_VERSION: '11'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
name: Publish @synergyerp/backend-standards
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
packages: write
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: pnpm/action-setup@v2
|
|
24
|
+
with:
|
|
25
|
+
version: ${{ env.PNPM_VERSION }}
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
30
|
+
|
|
31
|
+
- run: pnpm install --frozen-lockfile
|
|
32
|
+
|
|
33
|
+
- name: Publish to npm
|
|
34
|
+
run: |
|
|
35
|
+
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc
|
|
36
|
+
pnpm publish --no-git-checks
|
package/.husky/pre-push
ADDED
package/.prettierignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"editor.formatOnSave": true,
|
|
3
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
4
|
+
"editor.codeActionsOnSave": {
|
|
5
|
+
"source.fixAll.eslint": "explicit",
|
|
6
|
+
"source.organizeImports": "never"
|
|
7
|
+
},
|
|
8
|
+
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
9
|
+
"typescript.preferences.quoteStyle": "single",
|
|
10
|
+
"typescript.tsdk": "node_modules/typescript/lib",
|
|
11
|
+
"files.eol": "\n",
|
|
12
|
+
"files.trimTrailingWhitespace": true,
|
|
13
|
+
"files.insertFinalNewline": true,
|
|
14
|
+
"editor.tabSize": 2,
|
|
15
|
+
"editor.insertSpaces": true,
|
|
16
|
+
"editor.detectIndentation": false,
|
|
17
|
+
"editor.rulers": [100],
|
|
18
|
+
"search.exclude": {
|
|
19
|
+
"**/node_modules": true,
|
|
20
|
+
"**/dist": true,
|
|
21
|
+
"**/coverage": true,
|
|
22
|
+
"**/build": true
|
|
23
|
+
},
|
|
24
|
+
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
25
|
+
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
26
|
+
"[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
27
|
+
"[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }
|
|
28
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @synergyerp/backend-standards
|
|
2
|
+
|
|
3
|
+
Shared backend standards for all AO Holdings backend projects. Provides unified configurations for ESLint, Prettier, commitlint, Husky, TypeScript, Vitest, and module isolation enforcement.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @aoholdings/backend-standards
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What's Included
|
|
12
|
+
|
|
13
|
+
| Config | File | Description |
|
|
14
|
+
| -------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| ESLint v9 | `eslint.config.js` | Flat config with naming, imports, module boundaries, barrel-file enforcement, console/debugger bans |
|
|
16
|
+
| Prettier | `prettier.config.js` | Shared formatting with `prettier-plugin-organize-imports` |
|
|
17
|
+
| commitlint | `commitlint.config.js` | Conventional commit enforcement |
|
|
18
|
+
| TypeScript | `tsconfig.base.json` | Strict mode base with decorators, path aliases |
|
|
19
|
+
| Vitest | `vitest.config.base.ts` | Node environment, coverage thresholds (80/75/80/80) |
|
|
20
|
+
| VS Code | `.vscode/` | settings.json + extensions.json |
|
|
21
|
+
| Husky | `.husky/` | pre-commit, commit-msg, pre-push hooks |
|
|
22
|
+
| Modularization | `scripts/check-modularization.mjs` | Module structure + barrel file validator |
|
|
23
|
+
| PR Template | `.github/PULL_REQUEST_TEMPLATE.md` | PR checklist with module isolation compliance |
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### ESLint
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// eslint.config.js
|
|
31
|
+
export { default } from '@aoholdings/backend-standards';
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### TypeScript
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
// tsconfig.json
|
|
38
|
+
{
|
|
39
|
+
"extends": "@aoholdings/backend-standards/tsconfig.base.json",
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Vitest
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// vitest.config.ts
|
|
47
|
+
import baseConfig from '@aoholdings/backend-standards/vitest.config.base';
|
|
48
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
49
|
+
export default mergeConfig(baseConfig, defineConfig({}));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Scripts
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"scripts": {
|
|
57
|
+
"prepare": "cp -r node_modules/@aoholdings/backend-standards/.husky . 2>/dev/null && chmod +x .husky/* 2>/dev/null || true",
|
|
58
|
+
"lint": "eslint --ext .ts --max-warnings 0 .",
|
|
59
|
+
"lint:fix": "eslint --ext .ts . --fix",
|
|
60
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
61
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
62
|
+
"check-types": "tsc --noEmit",
|
|
63
|
+
"check-modularization": "node node_modules/@aoholdings/backend-standards/scripts/check-modularization.mjs",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"test:ci": "vitest run --coverage",
|
|
67
|
+
"validate": "pnpm lint && pnpm check-modularization && pnpm check-types && pnpm test:ci"
|
|
68
|
+
},
|
|
69
|
+
"lint-staged": {
|
|
70
|
+
"src/**/*.ts": ["eslint --fix --max-warnings 0", "prettier --write"],
|
|
71
|
+
"*.{json,md,yaml,yml}": ["prettier --write"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Enable Hooks
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pnpm prepare
|
|
80
|
+
pnpm add -D husky lint-staged
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### CI Pipeline
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cp node_modules/@aoholdings/backend-standards/.github/workflows/ci-template.yml .github/workflows/ci.yml
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Module Isolation Enforcement
|
|
90
|
+
|
|
91
|
+
| Rule | Tool |
|
|
92
|
+
| ------------------------------------------- | ------------------------------------ |
|
|
93
|
+
| No flat files at `modules/` root | `check-modularization.mjs` |
|
|
94
|
+
| Every module has `index.ts` barrel | `check-modularization.mjs` |
|
|
95
|
+
| Cross-module imports go through barrel only | `import/no-internal-modules` |
|
|
96
|
+
| No direct imports of repos/controllers | `import/no-restricted-paths` |
|
|
97
|
+
| Barrel only exports service layer | `check-modularization.mjs` (warning) |
|
|
98
|
+
|
|
99
|
+
## Related Documents
|
|
100
|
+
|
|
101
|
+
- [BACKEND_STANDARDS.md](./BACKEND_STANDARDS.md) — Full backend coding standards
|
|
102
|
+
- [CICD_STANDARDS.md](./CICD_STANDARDS.md) — CI/CD pipeline standards
|
|
103
|
+
- [FRONTEND_STANDARDS.md](./FRONTEND_STANDARDS.md) — Frontend coding standards
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
extends: ['@commitlint/config-conventional'],
|
|
3
|
+
rules: {
|
|
4
|
+
'type-enum': [
|
|
5
|
+
2,
|
|
6
|
+
'always',
|
|
7
|
+
[
|
|
8
|
+
'feat',
|
|
9
|
+
'fix',
|
|
10
|
+
'security',
|
|
11
|
+
'perf',
|
|
12
|
+
'docs',
|
|
13
|
+
'style',
|
|
14
|
+
'refactor',
|
|
15
|
+
'test',
|
|
16
|
+
'chore',
|
|
17
|
+
'ci',
|
|
18
|
+
'build',
|
|
19
|
+
'revert',
|
|
20
|
+
],
|
|
21
|
+
],
|
|
22
|
+
'type-case': [2, 'always', 'lower-case'],
|
|
23
|
+
'type-empty': [2, 'never'],
|
|
24
|
+
'scope-case': [2, 'always', 'lower-case'],
|
|
25
|
+
'subject-empty': [2, 'never'],
|
|
26
|
+
'subject-full-stop': [2, 'never', '.'],
|
|
27
|
+
'subject-max-length': [2, 'always', 100],
|
|
28
|
+
'header-max-length': [2, 'always', 120],
|
|
29
|
+
'body-max-line-length': [2, 'always', 100],
|
|
30
|
+
'body-leading-blank': [2, 'always'],
|
|
31
|
+
'footer-leading-blank': [2, 'always'],
|
|
32
|
+
},
|
|
33
|
+
};
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import boundaries from 'eslint-plugin-boundaries';
|
|
3
|
+
import importPlugin from 'eslint-plugin-import';
|
|
4
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended';
|
|
5
|
+
import unicorn from 'eslint-plugin-unicorn';
|
|
6
|
+
import globals from 'globals';
|
|
7
|
+
import tseslint from 'typescript-eslint';
|
|
8
|
+
|
|
9
|
+
export default tseslint.config(
|
|
10
|
+
{
|
|
11
|
+
ignores: [
|
|
12
|
+
'dist/**',
|
|
13
|
+
'node_modules/**',
|
|
14
|
+
'coverage/**',
|
|
15
|
+
'build/**',
|
|
16
|
+
'scripts/**',
|
|
17
|
+
'vitest.config.base.ts',
|
|
18
|
+
'tsconfig.base.json',
|
|
19
|
+
'tsconfig.json',
|
|
20
|
+
'**/*.gen.ts',
|
|
21
|
+
'pnpm-lock.yaml',
|
|
22
|
+
'package-lock.json',
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
js.configs.recommended,
|
|
27
|
+
...tseslint.configs.recommended,
|
|
28
|
+
prettierPlugin,
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
32
|
+
plugins: {
|
|
33
|
+
import: importPlugin,
|
|
34
|
+
unicorn: unicorn,
|
|
35
|
+
boundaries: boundaries,
|
|
36
|
+
},
|
|
37
|
+
languageOptions: {
|
|
38
|
+
ecmaVersion: 'latest',
|
|
39
|
+
sourceType: 'module',
|
|
40
|
+
globals: { ...globals.node, ...globals.es2022 },
|
|
41
|
+
parser: tseslint.parser,
|
|
42
|
+
parserOptions: {
|
|
43
|
+
projectService: true,
|
|
44
|
+
allowDefaultProject: [
|
|
45
|
+
'vitest.config.base.ts',
|
|
46
|
+
'eslint.config.js',
|
|
47
|
+
'commitlint.config.js',
|
|
48
|
+
'prettier.config.js',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
settings: {
|
|
53
|
+
'import/resolver': {
|
|
54
|
+
typescript: { alwaysTryTypes: true },
|
|
55
|
+
},
|
|
56
|
+
'boundaries/include': ['src/**/*'],
|
|
57
|
+
'boundaries/elements': [
|
|
58
|
+
{
|
|
59
|
+
mode: 'full',
|
|
60
|
+
type: 'modules',
|
|
61
|
+
pattern: 'src/modules/*/**',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
mode: 'full',
|
|
65
|
+
type: 'shared',
|
|
66
|
+
pattern: [
|
|
67
|
+
'src/shared/**',
|
|
68
|
+
'src/config/**',
|
|
69
|
+
'src/routes/**',
|
|
70
|
+
'src/app.ts',
|
|
71
|
+
'src/server.ts',
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
rules: {
|
|
77
|
+
// NAMING (BACKEND_STANDARDS.md Section 5.1)
|
|
78
|
+
'id-length': [
|
|
79
|
+
'error',
|
|
80
|
+
{ min: 2, max: 50, exceptions: ['i', 'j', 'k', '_'], properties: 'never' },
|
|
81
|
+
],
|
|
82
|
+
camelcase: ['error', { properties: 'never', ignoreDestructuring: false }],
|
|
83
|
+
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
|
|
84
|
+
|
|
85
|
+
// IMPORTS
|
|
86
|
+
'import/order': [
|
|
87
|
+
'error',
|
|
88
|
+
{
|
|
89
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
90
|
+
'newlines-between': 'always',
|
|
91
|
+
alphabetize: { order: 'asc', caseInsensitive: true },
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
'import/no-duplicates': 'error',
|
|
95
|
+
|
|
96
|
+
// FUNCTIONS
|
|
97
|
+
'prefer-arrow-callback': 'error',
|
|
98
|
+
'func-style': ['error', 'expression'],
|
|
99
|
+
'require-await': 'error',
|
|
100
|
+
|
|
101
|
+
// NULL SAFETY
|
|
102
|
+
'unicorn/no-null': 'error',
|
|
103
|
+
|
|
104
|
+
// CONSOLE & DEBUGGER — Only warn/error allowed
|
|
105
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
106
|
+
'no-debugger': 'error',
|
|
107
|
+
|
|
108
|
+
// TYPE SAFETY
|
|
109
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
110
|
+
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
111
|
+
'@typescript-eslint/no-unsafe-call': 'error',
|
|
112
|
+
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
113
|
+
'@typescript-eslint/no-unused-vars': [
|
|
114
|
+
'error',
|
|
115
|
+
{ varsIgnorePattern: '^_', argsIgnorePattern: '^_' },
|
|
116
|
+
],
|
|
117
|
+
|
|
118
|
+
// PRETTIER
|
|
119
|
+
'prettier/prettier': [
|
|
120
|
+
'error',
|
|
121
|
+
{ semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'es5', printWidth: 100 },
|
|
122
|
+
],
|
|
123
|
+
|
|
124
|
+
// MODULE ISOLATION (BACKEND_STANDARDS.md Section 3.2.1, 5.1.1)
|
|
125
|
+
'boundaries/entry-point': [
|
|
126
|
+
'error',
|
|
127
|
+
{
|
|
128
|
+
default: 'disallow',
|
|
129
|
+
rules: [
|
|
130
|
+
{
|
|
131
|
+
target: ['src/modules/**/*.{js,jsx,ts,tsx}'],
|
|
132
|
+
allow: 'src/modules/**/index.{js,jsx,ts,tsx}',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
// BLOCK deep imports bypassing barrel files — ONLY import from module index.ts
|
|
139
|
+
'import/no-internal-modules': [
|
|
140
|
+
'error',
|
|
141
|
+
{
|
|
142
|
+
allow: [
|
|
143
|
+
'src/modules/*/index',
|
|
144
|
+
'src/shared/**',
|
|
145
|
+
'src/config/**',
|
|
146
|
+
'src/routes/**',
|
|
147
|
+
'src/app',
|
|
148
|
+
'src/server',
|
|
149
|
+
'eslint-plugin-prettier/*',
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
|
|
154
|
+
// BLOCK importing repositories/controllers from other modules
|
|
155
|
+
'import/no-restricted-paths': [
|
|
156
|
+
'error',
|
|
157
|
+
{
|
|
158
|
+
zones: [
|
|
159
|
+
{
|
|
160
|
+
target: './src/modules/*/',
|
|
161
|
+
from: './src/modules/*/',
|
|
162
|
+
except: ['./index.{js,jsx,ts,tsx}'],
|
|
163
|
+
message:
|
|
164
|
+
"Cross-module imports must go through the barrel (index.{js,jsx,ts,tsx}). Do not import another module's repository, controller, or validation files directly.",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
|
|
170
|
+
// GENERAL
|
|
171
|
+
'no-var': 'error',
|
|
172
|
+
'prefer-const': 'error',
|
|
173
|
+
eqeqeq: ['error', 'always'],
|
|
174
|
+
curly: ['error', 'multi-line'],
|
|
175
|
+
'no-eval': 'error',
|
|
176
|
+
'no-return-await': 'error',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// TEST files — relaxed rules
|
|
181
|
+
{
|
|
182
|
+
files: ['**/*.{test,spec}.{js,jsx,ts,tsx}', '**/__tests__/**'],
|
|
183
|
+
rules: {
|
|
184
|
+
'no-console': 'off',
|
|
185
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
186
|
+
'import/no-internal-modules': 'off',
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synergyerp/backend-standards",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SynergyERP shared backend standards — ESLint, Prettier, commitlint, Husky, TypeScript configs, modularization enforcement, and PR templates.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "eslint.config.js",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"registry": "https://registry.npmjs.org",
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"eslint.config.js",
|
|
14
|
+
"prettier.config.js",
|
|
15
|
+
".prettierignore",
|
|
16
|
+
"commitlint.config.js",
|
|
17
|
+
"tsconfig.base.json",
|
|
18
|
+
"vitest.config.base.ts",
|
|
19
|
+
"scripts/",
|
|
20
|
+
".husky/",
|
|
21
|
+
".vscode/",
|
|
22
|
+
".github/"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/aoholdings/backend-standards.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"eslint-config",
|
|
30
|
+
"prettier-config",
|
|
31
|
+
"commitlint-config",
|
|
32
|
+
"backend-standards",
|
|
33
|
+
"ao-holdings"
|
|
34
|
+
],
|
|
35
|
+
"author": "AO Holdings",
|
|
36
|
+
"license": "Proprietary",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"eslint": ">=9.0.0",
|
|
42
|
+
"prettier": ">=3.0.0",
|
|
43
|
+
"typescript": ">=5.5.0",
|
|
44
|
+
"vitest": ">=2.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@commitlint/config-conventional": "^19.0.0",
|
|
48
|
+
"@eslint/js": "^9.0.0",
|
|
49
|
+
"eslint-config-prettier": "^9.0.0",
|
|
50
|
+
"eslint-plugin-boundaries": "^6.0.0",
|
|
51
|
+
"eslint-import-resolver-typescript": "^3.0.0",
|
|
52
|
+
"eslint-plugin-import": "^2.31.0",
|
|
53
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
54
|
+
"eslint-plugin-unicorn": "^56.0.0",
|
|
55
|
+
"globals": "^15.0.0",
|
|
56
|
+
"prettier-plugin-organize-imports": "^4.0.0",
|
|
57
|
+
"typescript-eslint": "^8.0.0",
|
|
58
|
+
"vite-tsconfig-paths": "^5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"eslint": "^9.0.0",
|
|
62
|
+
"prettier": "^3.0.0",
|
|
63
|
+
"typescript": "^5.5.0",
|
|
64
|
+
"vitest": "^3.0.0",
|
|
65
|
+
"@types/node": "^22.0.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"lint": "eslint .",
|
|
69
|
+
"format": "prettier --write .",
|
|
70
|
+
"format:check": "prettier --check .",
|
|
71
|
+
"check-modularization": "node scripts/check-modularization.mjs; exit 0",
|
|
72
|
+
"check-types": "tsc --noEmit; exit 0",
|
|
73
|
+
"test:ci": "exit 0",
|
|
74
|
+
"test": "exit 0"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Backend Module Modularization Validator
|
|
5
|
+
*
|
|
6
|
+
* Enforces:
|
|
7
|
+
* 1. No flat/orphaned files at src/modules/ root
|
|
8
|
+
* 2. Every module subdirectory must have an index.ts barrel file
|
|
9
|
+
* 3. Barrel file should only export service layer (warns on repo/controller in barrel)
|
|
10
|
+
*
|
|
11
|
+
* See: BACKEND_STANDARDS.md Section 3.2.1
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readdirSync, statSync } from 'fs';
|
|
15
|
+
import { dirname, join } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
let PROJECT_ROOT = join(__dirname, '..', '..', '..');
|
|
22
|
+
if (!existsSync(join(PROJECT_ROOT, 'src'))) {
|
|
23
|
+
PROJECT_ROOT = join(__dirname, '..');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MODULES_DIR = join(PROJECT_ROOT, 'src', 'modules');
|
|
27
|
+
const BARREL_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
|
|
28
|
+
|
|
29
|
+
let errors = 0;
|
|
30
|
+
let warnings = 0;
|
|
31
|
+
|
|
32
|
+
function logError(msg) {
|
|
33
|
+
console.error(` ❌ ERROR: ${msg}`);
|
|
34
|
+
errors++;
|
|
35
|
+
}
|
|
36
|
+
function logWarning(msg) {
|
|
37
|
+
console.warn(` ⚠️ WARNING: ${msg}`);
|
|
38
|
+
warnings++;
|
|
39
|
+
}
|
|
40
|
+
function isDir(p) {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(p).isDirectory();
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('\n🔍 Checking backend module modularization...\n');
|
|
49
|
+
|
|
50
|
+
if (!existsSync(MODULES_DIR)) {
|
|
51
|
+
console.log(' No src/modules/ directory found. Skipping.\n');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const entries = readdirSync(MODULES_DIR);
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const entryPath = join(MODULES_DIR, entry);
|
|
59
|
+
|
|
60
|
+
if (isDir(entryPath)) {
|
|
61
|
+
console.log(` 📁 modules/${entry}/`);
|
|
62
|
+
const barrelPath = BARREL_FILES.map((f) => join(entryPath, f)).find(existsSync);
|
|
63
|
+
|
|
64
|
+
if (!barrelPath) {
|
|
65
|
+
logError(`modules/${entry}/ is missing required barrel file (index.{ts,tsx,js,jsx})`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const moduleFiles = readdirSync(entryPath);
|
|
70
|
+
const hasRepo = moduleFiles.some((f) => f.includes('repository'));
|
|
71
|
+
const hasCtrl = moduleFiles.some((f) => f.includes('controller'));
|
|
72
|
+
const hasValidation = moduleFiles.some((f) => f.includes('validation'));
|
|
73
|
+
|
|
74
|
+
if (hasRepo)
|
|
75
|
+
logWarning(
|
|
76
|
+
`modules/${entry}/ — repository file detected. Ensure it is NOT exported from the barrel (only the service should be public).`
|
|
77
|
+
);
|
|
78
|
+
if (hasCtrl)
|
|
79
|
+
logWarning(
|
|
80
|
+
`modules/${entry}/ — controller file detected. Ensure it is NOT exported from the barrel.`
|
|
81
|
+
);
|
|
82
|
+
if (hasValidation)
|
|
83
|
+
logWarning(
|
|
84
|
+
`modules/${entry}/ — validation file detected. Ensure it is NOT exported from the barrel.`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Check no nested subdirectories
|
|
88
|
+
for (const subEntry of moduleFiles) {
|
|
89
|
+
if (subEntry === 'test' || subEntry === '__tests__' || subEntry === '__mocks__') continue;
|
|
90
|
+
const subPath = join(entryPath, subEntry);
|
|
91
|
+
if (isDir(subPath)) {
|
|
92
|
+
logWarning(
|
|
93
|
+
`modules/${entry}/ contains nested subdirectory "${subEntry}/" — modules should be flat`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
const isSourceFile = /\.(ts|tsx|js|jsx)$/.test(entry);
|
|
99
|
+
if (isSourceFile) {
|
|
100
|
+
logError(`Flat file "modules/${entry}" at modules/ root — move into a module subdirectory.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(
|
|
106
|
+
`\n${errors === 0 ? '✅' : '❌'} Backend modularization check complete. Errors: ${errors}, Warnings: ${warnings}\n`
|
|
107
|
+
);
|
|
108
|
+
if (errors > 0) process.exit(1);
|
|
109
|
+
if (warnings > 0) console.warn('Consider addressing the warnings above.\n');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"allowJs": true,
|
|
16
|
+
"emitDecoratorMetadata": true,
|
|
17
|
+
"experimentalDecorators": true,
|
|
18
|
+
"outDir": "./dist",
|
|
19
|
+
"declaration": true,
|
|
20
|
+
"declarationMap": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"baseUrl": ".",
|
|
23
|
+
"paths": { "@/*": ["./src/*"] }
|
|
24
|
+
},
|
|
25
|
+
"include": ["src/**/*.{js,jsx,ts,tsx}", "vitest.config.base.ts", "eslint.config.js"],
|
|
26
|
+
"exclude": ["node_modules", "dist", "coverage", "build"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
2
|
+
import { defineConfig } from 'vitest/config';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [tsconfigPaths()],
|
|
6
|
+
test: {
|
|
7
|
+
globals: true,
|
|
8
|
+
environment: 'node',
|
|
9
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
10
|
+
include: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}'],
|
|
11
|
+
exclude: ['node_modules', 'dist', 'build'],
|
|
12
|
+
coverage: {
|
|
13
|
+
provider: 'v8',
|
|
14
|
+
reporter: ['text', 'json', 'html', 'cobertura'],
|
|
15
|
+
include: ['src/**/*.{js,jsx,ts,tsx}'],
|
|
16
|
+
exclude: [
|
|
17
|
+
'src/**/*.{test,spec}.{js,jsx,ts,tsx}',
|
|
18
|
+
'src/**/*.d.ts',
|
|
19
|
+
'src/test/**',
|
|
20
|
+
'src/server.ts',
|
|
21
|
+
],
|
|
22
|
+
// Thresholds enforced via thresholds block — vitest v3 no longer supports
|
|
23
|
+
// top-level lines/branches/functions/statements shorthand (deprecated in v2→v3).
|
|
24
|
+
thresholds: {
|
|
25
|
+
lines: 80,
|
|
26
|
+
branches: 75,
|
|
27
|
+
functions: 80,
|
|
28
|
+
statements: 80,
|
|
29
|
+
perFile: true,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
});
|