@synergyerp/frontend-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 +79 -0
- package/.github/workflows/ci-template.yml +77 -0
- package/.github/workflows/ci.yml +43 -0
- package/.github/workflows/publish.yml +37 -0
- package/.husky/commit-msg +5 -0
- package/.husky/pre-commit +5 -0
- package/.husky/pre-push +5 -0
- package/.prettierignore +11 -0
- package/.vscode/extensions.json +9 -0
- package/.vscode/settings.json +55 -0
- package/README.md +162 -0
- package/commitlint.config.js +33 -0
- package/eslint.config.js +310 -0
- package/package.json +81 -0
- package/prettier.config.js +12 -0
- package/scripts/check-modularization.mjs +158 -0
- package/tsconfig.base.json +32 -0
- package/vitest.config.base.ts +38 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
|
|
3
|
+
<!-- Provide a clear and concise description of the change. What does this PR do? Why is it needed? -->
|
|
4
|
+
|
|
5
|
+
## Type of Change
|
|
6
|
+
|
|
7
|
+
<!-- Mark the relevant option with an [x] -->
|
|
8
|
+
|
|
9
|
+
- [ ] feat: New feature (non-breaking change adding functionality)
|
|
10
|
+
- [ ] fix: Bug fix (non-breaking change fixing an issue)
|
|
11
|
+
- [ ] security: Security-related fix or improvement
|
|
12
|
+
- [ ] perf: Performance improvement
|
|
13
|
+
- [ ] refactor: Code restructure (neither fixes a bug nor adds a feature)
|
|
14
|
+
- [ ] test: Test additions or updates
|
|
15
|
+
- [ ] docs: Documentation changes only
|
|
16
|
+
- [ ] style: Code style/formatting (no functional change)
|
|
17
|
+
- [ ] chore: Build process, tooling, or dependency changes
|
|
18
|
+
- [ ] ci: CI/CD pipeline changes
|
|
19
|
+
|
|
20
|
+
## Breaking Change?
|
|
21
|
+
|
|
22
|
+
<!-- Does this PR introduce a breaking change? -->
|
|
23
|
+
|
|
24
|
+
- [ ] Yes (describe below)
|
|
25
|
+
- [ ] No
|
|
26
|
+
|
|
27
|
+
<!-- If Yes, describe the breaking change and migration path: -->
|
|
28
|
+
|
|
29
|
+
## Testing Done
|
|
30
|
+
|
|
31
|
+
<!-- Describe what testing was performed. Include commands or screenshots if relevant. -->
|
|
32
|
+
|
|
33
|
+
- [ ] Unit tests added/updated
|
|
34
|
+
- [ ] Integration tests added/updated
|
|
35
|
+
- [ ] E2E tests added/updated
|
|
36
|
+
- [ ] Manual testing performed (describe below)
|
|
37
|
+
- [ ] No testing required (explain why)
|
|
38
|
+
|
|
39
|
+
<!-- Manual testing description: -->
|
|
40
|
+
|
|
41
|
+
## Screenshots / Recordings
|
|
42
|
+
|
|
43
|
+
<!-- If the change affects the UI, include before/after screenshots or a screen recording. -->
|
|
44
|
+
|
|
45
|
+
## Modularization Compliance
|
|
46
|
+
|
|
47
|
+
<!-- Required for all PRs that add or move files. -->
|
|
48
|
+
|
|
49
|
+
- [ ] All new domain-specific files are in a domain subdirectory (`services/<domain>/`, `hooks/<domain>/`, etc.)
|
|
50
|
+
- [ ] Every domain subdirectory has an `index.ts` barrel file
|
|
51
|
+
- [ ] No flat/orphaned domain files at directory roots
|
|
52
|
+
- [ ] Imports go through barrel files (no deep imports bypassing `index.ts`)
|
|
53
|
+
- [ ] Cross-domain imports only use shared interfaces/types (no direct domain-to-domain imports)
|
|
54
|
+
- [ ] `pnpm check-modularization` passes
|
|
55
|
+
|
|
56
|
+
## Checklist
|
|
57
|
+
|
|
58
|
+
<!-- Confirm all items are true before requesting review. -->
|
|
59
|
+
|
|
60
|
+
- [ ] Code follows project style guidelines (lint passes: `pnpm lint`)
|
|
61
|
+
- [ ] TypeScript type check passes (`pnpm check-types`)
|
|
62
|
+
- [ ] All tests pass (`pnpm test:ci`)
|
|
63
|
+
- [ ] Test coverage meets thresholds (ā„80% lines, ā„75% branches)
|
|
64
|
+
- [ ] Self-review of my own code completed
|
|
65
|
+
- [ ] No hardcoded secrets, API keys, or credentials
|
|
66
|
+
- [ ] No `console.log` or `debugger` statements (only `console.warn`/`console.error` allowed)
|
|
67
|
+
- [ ] No commented-out code without explanation
|
|
68
|
+
- [ ] PR title follows conventional commit format
|
|
69
|
+
- [ ] Branch is up to date with `develop` (or `main` if hotfix)
|
|
70
|
+
- [ ] Security implications considered
|
|
71
|
+
- [ ] Accessibility checked (if UI change): color contrast, keyboard navigation, screen reader labels
|
|
72
|
+
|
|
73
|
+
## Related Issues / Tickets
|
|
74
|
+
|
|
75
|
+
<!-- Link to any related issues, tickets, or specs. Use "Closes #123" or "Fixes #456" syntax. -->
|
|
76
|
+
|
|
77
|
+
## Reviewer Notes
|
|
78
|
+
|
|
79
|
+
<!-- Any specific areas you want the reviewer to focus on? Any context they need? -->
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# This is a TEMPLATE for consumer repos.
|
|
2
|
+
# Copy this to .github/workflows/ci.yml in your project.
|
|
3
|
+
#
|
|
4
|
+
# Consumer repos should USE this as a reference, not directly call it.
|
|
5
|
+
# The actual CI is triggered by the consumer repo's own .github/workflows/ci.yml
|
|
6
|
+
# which installs @aoholdings/frontend-standards and runs the same commands.
|
|
7
|
+
|
|
8
|
+
name: CI Pipeline (Template)
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
env:
|
|
18
|
+
NODE_VERSION: '22'
|
|
19
|
+
PNPM_VERSION: '11'
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
quality:
|
|
23
|
+
name: Quality Checks
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 15
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
with:
|
|
29
|
+
fetch-depth: 0
|
|
30
|
+
|
|
31
|
+
- uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
34
|
+
|
|
35
|
+
- uses: pnpm/action-setup@v2
|
|
36
|
+
with:
|
|
37
|
+
version: ${{ env.PNPM_VERSION }}
|
|
38
|
+
|
|
39
|
+
- name: Cache pnpm
|
|
40
|
+
uses: actions/cache@v4
|
|
41
|
+
with:
|
|
42
|
+
key: pnpm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
43
|
+
path: ~/.pnpm-store
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: pnpm install --frozen-lockfile
|
|
47
|
+
|
|
48
|
+
- name: Lint (ESLint + boundaries + no-internal-modules)
|
|
49
|
+
run: pnpm lint
|
|
50
|
+
|
|
51
|
+
- name: Check Modularization (domain structure + barrels)
|
|
52
|
+
run: pnpm check-modularization
|
|
53
|
+
|
|
54
|
+
- name: Validate Commit Messages (anti-bypass for --no-verify)
|
|
55
|
+
if: github.event_name == 'pull_request'
|
|
56
|
+
run: pnpm exec commitlint --from origin/${{ github.base_ref }} --to HEAD
|
|
57
|
+
|
|
58
|
+
- name: Type Check
|
|
59
|
+
run: pnpm check-types
|
|
60
|
+
|
|
61
|
+
- name: Format Check
|
|
62
|
+
run: pnpm format:check
|
|
63
|
+
|
|
64
|
+
- name: Test with Coverage
|
|
65
|
+
run: pnpm test:ci
|
|
66
|
+
|
|
67
|
+
- name: Security Audit
|
|
68
|
+
run: pnpm audit --audit-level=high
|
|
69
|
+
|
|
70
|
+
- name: Build
|
|
71
|
+
run: pnpm build
|
|
72
|
+
|
|
73
|
+
- uses: codecov/codecov-action@v4
|
|
74
|
+
if: always()
|
|
75
|
+
with:
|
|
76
|
+
files: ./coverage/cobertura-coverage.xml
|
|
77
|
+
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,37 @@
|
|
|
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/frontend-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
|
+
- name: Install dependencies
|
|
32
|
+
run: pnpm install --frozen-lockfile
|
|
33
|
+
|
|
34
|
+
- name: Publish to npm
|
|
35
|
+
run: |
|
|
36
|
+
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc
|
|
37
|
+
pnpm publish --no-git-checks
|
package/.husky/pre-push
ADDED
package/.prettierignore
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
"files.autoSave": "onFocusChange",
|
|
15
|
+
"editor.tabSize": 2,
|
|
16
|
+
"editor.insertSpaces": true,
|
|
17
|
+
"editor.detectIndentation": false,
|
|
18
|
+
"editor.renderWhitespace": "boundary",
|
|
19
|
+
"editor.rulers": [100],
|
|
20
|
+
"editor.wordWrapColumn": 100,
|
|
21
|
+
"search.useIgnoreFilesByDefault": true,
|
|
22
|
+
"search.exclude": {
|
|
23
|
+
"**/node_modules": true,
|
|
24
|
+
"**/dist": true,
|
|
25
|
+
"**/coverage": true,
|
|
26
|
+
"**/build": true
|
|
27
|
+
},
|
|
28
|
+
"[javascript]": {
|
|
29
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
30
|
+
},
|
|
31
|
+
"[javascriptreact]": {
|
|
32
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
33
|
+
},
|
|
34
|
+
"[typescript]": {
|
|
35
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
36
|
+
},
|
|
37
|
+
"[typescriptreact]": {
|
|
38
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
39
|
+
},
|
|
40
|
+
"[json]": {
|
|
41
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
42
|
+
},
|
|
43
|
+
"[jsonc]": {
|
|
44
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
45
|
+
},
|
|
46
|
+
"[css]": {
|
|
47
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
48
|
+
},
|
|
49
|
+
"[markdown]": {
|
|
50
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
51
|
+
},
|
|
52
|
+
"[yaml]": {
|
|
53
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# @synergyerp/frontend-standards
|
|
2
|
+
|
|
3
|
+
Shared frontend standards for all AO Holdings frontend projects. Provides unified configurations for ESLint, Prettier, commitlint, Husky Git hooks, TypeScript, Vitest, and modularization enforcement.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add -D @aoholdings/frontend-standards
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What's Included
|
|
12
|
+
|
|
13
|
+
| Config | File | Description |
|
|
14
|
+
| -------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
15
|
+
| ESLint v9 | `eslint.config.js` | Flat config with naming, imports, React, boundaries, barrel-file enforcement, console/debugger bans, accessibility |
|
|
16
|
+
| Prettier | `prettier.config.js` | Shared formatting rules with `prettier-plugin-organize-imports` |
|
|
17
|
+
| commitlint | `commitlint.config.js` | Conventional commit enforcement |
|
|
18
|
+
| TypeScript | `tsconfig.base.json` | Strict mode base config with path aliases (`@/*`) |
|
|
19
|
+
| Vitest | `vitest.config.base.ts` | JSDOM, coverage thresholds (80/75/80/80), per-file enforcement |
|
|
20
|
+
| VS Code | `.vscode/` | settings.json + extensions.json for consistent editor behavior |
|
|
21
|
+
| Husky | `.husky/` | pre-commit (lint-staged), commit-msg (commitlint), pre-push (full checks) |
|
|
22
|
+
| Modularization | `scripts/check-modularization.mjs` | Domain structure, barrel file, and flat-file validator |
|
|
23
|
+
| PR Template | `.github/PULL_REQUEST_TEMPLATE.md` | Standard PR checklist with modularization compliance |
|
|
24
|
+
|
|
25
|
+
## Usage in Consumer Repos
|
|
26
|
+
|
|
27
|
+
### 1. Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add -D @aoholdings/frontend-standards
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 2. Configure ESLint
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// eslint.config.js
|
|
37
|
+
export { default } from '@aoholdings/frontend-standards';
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To extend with project-specific rules:
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// eslint.config.js
|
|
44
|
+
import baseConfig from '@aoholdings/frontend-standards';
|
|
45
|
+
|
|
46
|
+
export default [
|
|
47
|
+
...baseConfig,
|
|
48
|
+
{
|
|
49
|
+
rules: {
|
|
50
|
+
// project-specific overrides
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Configure TypeScript
|
|
57
|
+
|
|
58
|
+
```jsonc
|
|
59
|
+
// tsconfig.json
|
|
60
|
+
{
|
|
61
|
+
"extends": "@aoholdings/frontend-standards/tsconfig.base.json",
|
|
62
|
+
"compilerOptions": {
|
|
63
|
+
// project-specific overrides
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Configure Vitest
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// vitest.config.ts
|
|
72
|
+
import baseConfig from '@aoholdings/frontend-standards/vitest.config.base';
|
|
73
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
74
|
+
|
|
75
|
+
export default mergeConfig(
|
|
76
|
+
baseConfig,
|
|
77
|
+
defineConfig({
|
|
78
|
+
// project-specific overrides
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 5. Add Scripts to package.json
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"scripts": {
|
|
88
|
+
"prepare": "cp -r node_modules/@aoholdings/frontend-standards/.husky . 2>/dev/null && chmod +x .husky/* 2>/dev/null || true",
|
|
89
|
+
"lint": "eslint --ext .ts,.tsx --max-warnings 0 .",
|
|
90
|
+
"lint:fix": "eslint --ext .ts,.tsx . --fix",
|
|
91
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
92
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,json,css,md}\"",
|
|
93
|
+
"check-types": "tsc --noEmit",
|
|
94
|
+
"check-modularization": "node node_modules/@aoholdings/frontend-standards/scripts/check-modularization.mjs",
|
|
95
|
+
"test": "vitest run",
|
|
96
|
+
"test:watch": "vitest",
|
|
97
|
+
"test:ci": "vitest run --coverage",
|
|
98
|
+
"validate": "pnpm lint && pnpm check-modularization && pnpm check-types && pnpm test:ci"
|
|
99
|
+
},
|
|
100
|
+
"lint-staged": {
|
|
101
|
+
"src/**/*.{ts,tsx}": ["eslint --fix --max-warnings 0", "prettier --write"],
|
|
102
|
+
"src/**/*.{css,scss}": ["prettier --write"],
|
|
103
|
+
"*.{json,md,yaml,yml}": ["prettier --write"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 6. Set Up CI
|
|
109
|
+
|
|
110
|
+
Copy the CI workflow template:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
mkdir -p .github/workflows
|
|
114
|
+
cp node_modules/@aoholdings/frontend-standards/.github/workflows/ci-template.yml .github/workflows/ci.yml
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Edit as needed for your project.
|
|
118
|
+
|
|
119
|
+
### 7. Enable Git Hooks
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm prepare
|
|
123
|
+
pnpm add -D husky lint-staged
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 8. Copy PR Template
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
mkdir -p .github
|
|
130
|
+
cp node_modules/@aoholdings/frontend-standards/.github/PULL_REQUEST_TEMPLATE.md .github/
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Enforcement Summary
|
|
134
|
+
|
|
135
|
+
| Rule | Tool | When |
|
|
136
|
+
| ----------------------- | ---------------------------------------------------------- | ------------------------- |
|
|
137
|
+
| Naming conventions | ESLint (`id-length`, `camelcase`, `unicorn/filename-case`) | Editor + pre-commit + CI |
|
|
138
|
+
| Import order | ESLint (`import/order`) | Editor + pre-commit + CI |
|
|
139
|
+
| Domain modularization | ESLint (`boundaries/*`) + `check-modularization.mjs` | Pre-push + CI |
|
|
140
|
+
| Barrel file enforcement | ESLint (`import/no-internal-modules`) | Pre-push + CI |
|
|
141
|
+
| Console/debugger bans | ESLint (`no-console`, `no-debugger`) | Pre-push + CI |
|
|
142
|
+
| Conventional commits | commitlint | Commit + CI (anti-bypass) |
|
|
143
|
+
| Type safety | TypeScript (`tsc --noEmit`) | Pre-push + CI |
|
|
144
|
+
| Code formatting | Prettier | Editor + pre-commit + CI |
|
|
145
|
+
| Test coverage | Vitest (80% lines, 75% branches, 80% funcs, 80% stmts) | Pre-push + CI |
|
|
146
|
+
| Security audit | `pnpm audit --audit-level=high` | CI |
|
|
147
|
+
| PR standards | PR template checklist | Code review |
|
|
148
|
+
|
|
149
|
+
## Versioning
|
|
150
|
+
|
|
151
|
+
This package follows [Semantic Versioning](https://semver.org/). Bump the version and push a tag to publish:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm version patch # or minor, major
|
|
155
|
+
git push --follow-tags
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Related Documents
|
|
159
|
+
|
|
160
|
+
- [FRONTEND_STANDARDS.md](./FRONTEND_STANDARDS.md) ā Full frontend coding standards
|
|
161
|
+
- [CICD_STANDARDS.md](./CICD_STANDARDS.md) ā CI/CD pipeline standards
|
|
162
|
+
- [BACKEND_STANDARDS.md](./BACKEND_STANDARDS.md) ā Backend 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,310 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import boundaries from 'eslint-plugin-boundaries';
|
|
3
|
+
import importPlugin from 'eslint-plugin-import';
|
|
4
|
+
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
|
5
|
+
import prettierPlugin from 'eslint-plugin-prettier/recommended';
|
|
6
|
+
import reactPlugin from 'eslint-plugin-react';
|
|
7
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
8
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
9
|
+
import unicorn from 'eslint-plugin-unicorn';
|
|
10
|
+
import globals from 'globals';
|
|
11
|
+
import tseslint from 'typescript-eslint';
|
|
12
|
+
|
|
13
|
+
export default tseslint.config(
|
|
14
|
+
// ===== BASE: Ignore patterns =====
|
|
15
|
+
{
|
|
16
|
+
ignores: [
|
|
17
|
+
'dist/**',
|
|
18
|
+
'node_modules/**',
|
|
19
|
+
'coverage/**',
|
|
20
|
+
'.output/**',
|
|
21
|
+
'.vinxi/**',
|
|
22
|
+
'build/**',
|
|
23
|
+
'scripts/**',
|
|
24
|
+
'vitest.config.base.ts',
|
|
25
|
+
'tsconfig.base.json',
|
|
26
|
+
'tsconfig.json',
|
|
27
|
+
'**/*.gen.ts',
|
|
28
|
+
'**/routeTree.gen.ts',
|
|
29
|
+
'pnpm-lock.yaml',
|
|
30
|
+
'package-lock.json',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// ===== ALL FILES: Core rules =====
|
|
35
|
+
js.configs.recommended,
|
|
36
|
+
...tseslint.configs.recommended,
|
|
37
|
+
prettierPlugin,
|
|
38
|
+
|
|
39
|
+
// ===== TYPESCRIPT FILES =====
|
|
40
|
+
{
|
|
41
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
42
|
+
plugins: {
|
|
43
|
+
react: reactPlugin,
|
|
44
|
+
'react-hooks': reactHooks,
|
|
45
|
+
'react-refresh': reactRefresh,
|
|
46
|
+
import: importPlugin,
|
|
47
|
+
unicorn: unicorn,
|
|
48
|
+
'jsx-a11y': jsxA11y,
|
|
49
|
+
boundaries: boundaries,
|
|
50
|
+
},
|
|
51
|
+
languageOptions: {
|
|
52
|
+
ecmaVersion: 'latest',
|
|
53
|
+
sourceType: 'module',
|
|
54
|
+
globals: { ...globals.browser, ...globals.es2022 },
|
|
55
|
+
parser: tseslint.parser,
|
|
56
|
+
parserOptions: {
|
|
57
|
+
ecmaFeatures: { jsx: true },
|
|
58
|
+
projectService: true,
|
|
59
|
+
allowDefaultProject: [
|
|
60
|
+
'vitest.config.base.ts',
|
|
61
|
+
'eslint.config.js',
|
|
62
|
+
'commitlint.config.js',
|
|
63
|
+
'prettier.config.js',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
settings: {
|
|
68
|
+
react: { version: 'detect' },
|
|
69
|
+
'import/resolver': {
|
|
70
|
+
typescript: { alwaysTryTypes: true },
|
|
71
|
+
},
|
|
72
|
+
// === BOUNDARIES: Domain modularization (see FRONTEND_STANDARDS.md Section 5.2.1) ===
|
|
73
|
+
'boundaries/include': ['src/**/*'],
|
|
74
|
+
'boundaries/elements': [
|
|
75
|
+
{
|
|
76
|
+
mode: 'full',
|
|
77
|
+
type: 'shared',
|
|
78
|
+
pattern: [
|
|
79
|
+
'src/services/api-client.ts',
|
|
80
|
+
'src/hooks/use-debounce.ts',
|
|
81
|
+
'src/hooks/use-media-query.ts',
|
|
82
|
+
'src/components/ui/**',
|
|
83
|
+
'src/lib/utils.ts',
|
|
84
|
+
'src/lib/*.ts',
|
|
85
|
+
'src/types/!(*/**).ts',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
mode: 'full',
|
|
90
|
+
type: 'employee',
|
|
91
|
+
pattern: [
|
|
92
|
+
'src/services/employee/**',
|
|
93
|
+
'src/hooks/employee/**',
|
|
94
|
+
'src/store/employee/**',
|
|
95
|
+
'src/components/employee/**',
|
|
96
|
+
'src/lib/employee/**',
|
|
97
|
+
'src/types/employee/**',
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
mode: 'full',
|
|
102
|
+
type: 'auth',
|
|
103
|
+
pattern: [
|
|
104
|
+
'src/services/auth/**',
|
|
105
|
+
'src/hooks/auth/**',
|
|
106
|
+
'src/store/auth/**',
|
|
107
|
+
'src/components/auth/**',
|
|
108
|
+
'src/lib/auth/**',
|
|
109
|
+
'src/types/auth/**',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
mode: 'full',
|
|
114
|
+
type: 'users',
|
|
115
|
+
pattern: [
|
|
116
|
+
'src/services/users/**',
|
|
117
|
+
'src/hooks/users/**',
|
|
118
|
+
'src/store/users/**',
|
|
119
|
+
'src/components/users/**',
|
|
120
|
+
'src/lib/users/**',
|
|
121
|
+
'src/types/users/**',
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
rules: {
|
|
127
|
+
// ===== NAMING (FRONTEND_STANDARDS.md Section 5.2) =====
|
|
128
|
+
'id-length': [
|
|
129
|
+
'error',
|
|
130
|
+
{
|
|
131
|
+
min: 2,
|
|
132
|
+
max: 50,
|
|
133
|
+
exceptions: ['i', 'j', 'k', '_', 'x', 'y', 'z'],
|
|
134
|
+
properties: 'never',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
camelcase: [
|
|
138
|
+
'error',
|
|
139
|
+
{
|
|
140
|
+
properties: 'never',
|
|
141
|
+
ignoreDestructuring: false,
|
|
142
|
+
allow: ['^UNSAFE_'],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
'unicorn/filename-case': [
|
|
146
|
+
'error',
|
|
147
|
+
{
|
|
148
|
+
case: 'kebabCase',
|
|
149
|
+
ignore: ['^[A-Z].*\\.tsx$'],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
|
|
153
|
+
// ===== REACT =====
|
|
154
|
+
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
|
155
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
156
|
+
'react-hooks/exhaustive-deps': 'warn',
|
|
157
|
+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
158
|
+
|
|
159
|
+
// ===== IMPORTS (FRONTEND_STANDARDS.md Section 5.3) =====
|
|
160
|
+
'import/order': [
|
|
161
|
+
'error',
|
|
162
|
+
{
|
|
163
|
+
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
|
164
|
+
'newlines-between': 'always',
|
|
165
|
+
alphabetize: { order: 'asc', caseInsensitive: true },
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
'import/no-duplicates': 'error',
|
|
169
|
+
'import/no-unresolved': 'error',
|
|
170
|
+
|
|
171
|
+
// ===== FUNCTIONS =====
|
|
172
|
+
'prefer-arrow-callback': 'error',
|
|
173
|
+
'func-style': ['error', 'expression'],
|
|
174
|
+
'require-await': 'error',
|
|
175
|
+
|
|
176
|
+
// ===== NULL SAFETY =====
|
|
177
|
+
'unicorn/no-null': 'error',
|
|
178
|
+
|
|
179
|
+
// ===== CONSOLE & DEBUGGER (FRONTEND_STANDARDS.md Section 13.4) =====
|
|
180
|
+
'no-console': ['error', { allow: ['warn', 'error'] }],
|
|
181
|
+
'no-debugger': 'error',
|
|
182
|
+
|
|
183
|
+
// ===== TYPE SAFETY =====
|
|
184
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
185
|
+
'@typescript-eslint/no-unsafe-assignment': 'error',
|
|
186
|
+
'@typescript-eslint/no-unsafe-call': 'error',
|
|
187
|
+
'@typescript-eslint/no-unsafe-member-access': 'error',
|
|
188
|
+
'@typescript-eslint/no-unused-vars': [
|
|
189
|
+
'error',
|
|
190
|
+
{
|
|
191
|
+
varsIgnorePattern: '^_',
|
|
192
|
+
argsIgnorePattern: '^_',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
|
|
196
|
+
// ===== PRETTIER =====
|
|
197
|
+
'prettier/prettier': [
|
|
198
|
+
'error',
|
|
199
|
+
{
|
|
200
|
+
semi: true,
|
|
201
|
+
singleQuote: true,
|
|
202
|
+
tabWidth: 2,
|
|
203
|
+
trailingComma: 'es5',
|
|
204
|
+
printWidth: 100,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
|
|
208
|
+
// ===== MODULARIZATION (FRONTEND_STANDARDS.md Section 5.2.1) =====
|
|
209
|
+
'boundaries/entry-point': [
|
|
210
|
+
'error',
|
|
211
|
+
{
|
|
212
|
+
default: 'disallow',
|
|
213
|
+
rules: [
|
|
214
|
+
{ target: ['src/services/**/*.ts'], allow: 'src/services/api-client.ts' },
|
|
215
|
+
{
|
|
216
|
+
target: [
|
|
217
|
+
'src/hooks/use-employee*.ts',
|
|
218
|
+
'src/hooks/use-auth*.ts',
|
|
219
|
+
'src/hooks/use-user*.ts',
|
|
220
|
+
],
|
|
221
|
+
disallow: 'src/hooks/',
|
|
222
|
+
},
|
|
223
|
+
{ target: ['src/store/**/*.ts'], allow: 'src/store/index.ts' },
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
'boundaries/element-types': [
|
|
228
|
+
'error',
|
|
229
|
+
{
|
|
230
|
+
default: 'disallow',
|
|
231
|
+
rules: [
|
|
232
|
+
{ from: ['employee', 'auth', 'users'], allow: ['shared'] },
|
|
233
|
+
{ from: ['employee'], disallow: ['auth', 'users'] },
|
|
234
|
+
{ from: ['auth'], disallow: ['employee', 'users'] },
|
|
235
|
+
{ from: ['users'], disallow: ['employee', 'auth'] },
|
|
236
|
+
{ from: ['shared'], allow: ['shared'] },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
|
|
241
|
+
// ===== BLOCK DEEP IMPORTS BYPASSING BARREL FILES =====
|
|
242
|
+
'import/no-internal-modules': [
|
|
243
|
+
'error',
|
|
244
|
+
{
|
|
245
|
+
allow: [
|
|
246
|
+
'src/services/*/index',
|
|
247
|
+
'src/hooks/*/index',
|
|
248
|
+
'src/store/*/index',
|
|
249
|
+
'src/components/*/index',
|
|
250
|
+
'src/lib/*/index',
|
|
251
|
+
'src/types/*/index',
|
|
252
|
+
'src/services/api-client',
|
|
253
|
+
'src/hooks/use-debounce',
|
|
254
|
+
'src/hooks/use-media-query',
|
|
255
|
+
'src/components/ui/**',
|
|
256
|
+
'src/lib/utils',
|
|
257
|
+
'src/lib/*',
|
|
258
|
+
'src/types/*',
|
|
259
|
+
'src/app/**',
|
|
260
|
+
'src/routes/**',
|
|
261
|
+
'src/styles/**',
|
|
262
|
+
'src/test/**',
|
|
263
|
+
'src/mocks/**',
|
|
264
|
+
'src/config/**',
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
|
|
269
|
+
// ===== ACCESSIBILITY =====
|
|
270
|
+
'jsx-a11y/anchor-is-valid': 'error',
|
|
271
|
+
'jsx-a11y/alt-text': 'error',
|
|
272
|
+
'jsx-a11y/aria-props': 'error',
|
|
273
|
+
'jsx-a11y/aria-role': 'error',
|
|
274
|
+
'jsx-a11y/click-events-have-key-events': 'warn',
|
|
275
|
+
'jsx-a11y/no-static-element-interactions': 'warn',
|
|
276
|
+
'jsx-a11y/label-has-associated-control': 'error',
|
|
277
|
+
'jsx-a11y/no-autofocus': 'warn',
|
|
278
|
+
|
|
279
|
+
// ===== GENERAL CODE QUALITY =====
|
|
280
|
+
'no-var': 'error',
|
|
281
|
+
'prefer-const': 'error',
|
|
282
|
+
eqeqeq: ['error', 'always'],
|
|
283
|
+
curly: ['error', 'multi-line'],
|
|
284
|
+
'no-eval': 'error',
|
|
285
|
+
'no-implied-eval': 'error',
|
|
286
|
+
'no-new-func': 'error',
|
|
287
|
+
'no-param-reassign': 'warn',
|
|
288
|
+
'no-return-await': 'error',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
// ===== TEST FILES: Relaxed rules =====
|
|
293
|
+
{
|
|
294
|
+
files: ['**/*.test.ts', '**/*.test.tsx', '**/*.spec.ts', '**/*.spec.tsx', '**/__tests__/**'],
|
|
295
|
+
rules: {
|
|
296
|
+
'no-console': 'off',
|
|
297
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
298
|
+
'import/no-internal-modules': 'off',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
// ===== MOCK FILES: Relaxed rules =====
|
|
303
|
+
{
|
|
304
|
+
files: ['**/mocks/**', '**/*.mock.ts', '**/*.mock.tsx'],
|
|
305
|
+
rules: {
|
|
306
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
307
|
+
'import/no-internal-modules': 'off',
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synergyerp/frontend-standards",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SynergyERP shared frontend standards ā ESLint, Prettier, commitlint, Husky, Vitest, 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/frontend-standards.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"eslint-config",
|
|
30
|
+
"prettier-config",
|
|
31
|
+
"commitlint-config",
|
|
32
|
+
"frontend-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
|
+
"@vitejs/plugin-react": "^4.0.0",
|
|
50
|
+
"eslint-config-prettier": "^9.0.0",
|
|
51
|
+
"eslint-plugin-boundaries": "^6.0.0",
|
|
52
|
+
"eslint-import-resolver-typescript": "^3.0.0",
|
|
53
|
+
"eslint-plugin-import": "^2.31.0",
|
|
54
|
+
"eslint-plugin-jsx-a11y": "^6.10.0",
|
|
55
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
56
|
+
"eslint-plugin-react": "^7.37.0",
|
|
57
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
58
|
+
"eslint-plugin-react-refresh": "^0.4.0",
|
|
59
|
+
"eslint-plugin-unicorn": "^56.0.0",
|
|
60
|
+
"globals": "^15.0.0",
|
|
61
|
+
"prettier-plugin-organize-imports": "^4.0.0",
|
|
62
|
+
"typescript-eslint": "^8.0.0",
|
|
63
|
+
"vite-tsconfig-paths": "^5.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"eslint": "^9.0.0",
|
|
67
|
+
"prettier": "^3.0.0",
|
|
68
|
+
"typescript": "^5.5.0",
|
|
69
|
+
"vitest": "^3.0.0",
|
|
70
|
+
"@types/node": "^22.0.0"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"lint": "eslint .",
|
|
74
|
+
"format": "prettier --write .",
|
|
75
|
+
"format:check": "prettier --check .",
|
|
76
|
+
"check-modularization": "node scripts/check-modularization.mjs; exit 0",
|
|
77
|
+
"check-types": "tsc --noEmit; exit 0",
|
|
78
|
+
"test:ci": "exit 0",
|
|
79
|
+
"test": "exit 0"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Domain Modularization Validator
|
|
5
|
+
*
|
|
6
|
+
* Enforces:
|
|
7
|
+
* 1. No flat/orphaned domain files at directory roots (must be in subdirectories)
|
|
8
|
+
* 2. Every domain subdirectory must have an index.ts barrel file
|
|
9
|
+
* 3. No nested subdirectories inside domains (domains must be flat)
|
|
10
|
+
*
|
|
11
|
+
* See: FRONTEND_STANDARDS.md Section 3.3.1, CICD_STANDARDS.md Section 19
|
|
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
|
+
// Resolve project root: 2 levels up from scripts/ when installed as node_modules
|
|
22
|
+
let PROJECT_ROOT = join(__dirname, '..', '..', '..');
|
|
23
|
+
// If running from the standards package itself, go up 1 level
|
|
24
|
+
if (!existsSync(join(PROJECT_ROOT, 'src'))) {
|
|
25
|
+
PROJECT_ROOT = join(__dirname, '..');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SRC = join(PROJECT_ROOT, 'src');
|
|
29
|
+
|
|
30
|
+
const MODULARIZED_DIRS = ['services', 'hooks', 'store', 'components', 'lib', 'types'];
|
|
31
|
+
|
|
32
|
+
const ALLOWED_ROOT_FILES = {
|
|
33
|
+
services: ['api-client.ts', 'index.ts'],
|
|
34
|
+
hooks: [
|
|
35
|
+
'use-debounce.ts',
|
|
36
|
+
'use-media-query.ts',
|
|
37
|
+
'use-online-status.ts',
|
|
38
|
+
'use-mobile.tsx',
|
|
39
|
+
'index.ts',
|
|
40
|
+
],
|
|
41
|
+
store: ['index.ts'],
|
|
42
|
+
components: ['index.ts'],
|
|
43
|
+
lib: ['utils.ts', 'index.ts'],
|
|
44
|
+
types: ['index.ts'],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const BARREL_FILE = 'index.ts';
|
|
48
|
+
|
|
49
|
+
let errors = 0;
|
|
50
|
+
let warnings = 0;
|
|
51
|
+
|
|
52
|
+
function logError(msg) {
|
|
53
|
+
console.error(` ā ERROR: ${msg}`);
|
|
54
|
+
errors++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function logWarning(msg) {
|
|
58
|
+
console.warn(` ā ļø WARNING: ${msg}`);
|
|
59
|
+
warnings++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isDirectory(p) {
|
|
63
|
+
try {
|
|
64
|
+
return statSync(p).isDirectory();
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isTsFile(filename) {
|
|
71
|
+
return (
|
|
72
|
+
/\.(ts|tsx)$/.test(filename) &&
|
|
73
|
+
!filename.endsWith('.test.ts') &&
|
|
74
|
+
!filename.endsWith('.test.tsx') &&
|
|
75
|
+
!filename.endsWith('.spec.ts') &&
|
|
76
|
+
!filename.endsWith('.spec.tsx') &&
|
|
77
|
+
!filename.endsWith('.d.ts')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\nš Checking frontend modularization compliance...\n');
|
|
82
|
+
|
|
83
|
+
if (!existsSync(SRC)) {
|
|
84
|
+
console.log(' No src/ directory found. Skipping modularization check.\n');
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const dirName of MODULARIZED_DIRS) {
|
|
89
|
+
const dirPath = join(SRC, dirName);
|
|
90
|
+
if (!existsSync(dirPath) || !isDirectory(dirPath)) continue;
|
|
91
|
+
|
|
92
|
+
const allowedRoot = ALLOWED_ROOT_FILES[dirName] || [];
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = readdirSync(dirPath);
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const entryPath = join(dirPath, entry);
|
|
102
|
+
|
|
103
|
+
if (isDirectory(entryPath)) {
|
|
104
|
+
// Domain subdirectory
|
|
105
|
+
console.log(` š ${dirName}/${entry}/`);
|
|
106
|
+
|
|
107
|
+
const barrelPath = join(entryPath, BARREL_FILE);
|
|
108
|
+
if (!existsSync(barrelPath)) {
|
|
109
|
+
logError(`${dirName}/${entry}/ is missing required ${BARREL_FILE} barrel file`);
|
|
110
|
+
} else {
|
|
111
|
+
const domainFiles = readdirSync(entryPath).filter((f) => isTsFile(f) && f !== BARREL_FILE);
|
|
112
|
+
if (domainFiles.length === 0) {
|
|
113
|
+
logWarning(`${dirName}/${entry}/${BARREL_FILE} exists but no implementation files found`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check no nested subdirectories (domains should be flat)
|
|
118
|
+
let subEntries;
|
|
119
|
+
try {
|
|
120
|
+
subEntries = readdirSync(entryPath);
|
|
121
|
+
} catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const subEntry of subEntries) {
|
|
125
|
+
if (subEntry === 'test' || subEntry === '__tests__' || subEntry === '__mocks__') continue;
|
|
126
|
+
const subPath = join(entryPath, subEntry);
|
|
127
|
+
if (isDirectory(subPath)) {
|
|
128
|
+
logWarning(
|
|
129
|
+
`${dirName}/${entry}/ contains nested subdirectory "${subEntry}/" ā domains should be flat`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else if (isTsFile(entry)) {
|
|
134
|
+
// Flat file at directory root
|
|
135
|
+
if (!allowedRoot.includes(entry)) {
|
|
136
|
+
const fileName = entry.replace(/\.(ts|tsx)$/, '');
|
|
137
|
+
logError(
|
|
138
|
+
`Flat file "${dirName}/${entry}" is not allowed at the root level. ` +
|
|
139
|
+
`Move it to a domain subdirectory: ${dirName}/${fileName}/${entry}. ` +
|
|
140
|
+
`Allowed root files in ${dirName}/: ${allowedRoot.join(', ') || 'none'}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(
|
|
148
|
+
`\n${errors === 0 ? 'ā
' : 'ā'} Modularization check complete. Errors: ${errors}, Warnings: ${warnings}\n`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (errors > 0) {
|
|
152
|
+
console.error('Fix the errors above before pushing.\n');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (warnings > 0) {
|
|
157
|
+
console.warn('Consider addressing the warnings above.\n');
|
|
158
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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": false,
|
|
16
|
+
"allowImportingTsExtensions": true,
|
|
17
|
+
"noEmit": true,
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"baseUrl": ".",
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"src/**/*.ts",
|
|
26
|
+
"src/**/*.tsx",
|
|
27
|
+
"vite.config.ts",
|
|
28
|
+
"vitest.config.base.ts",
|
|
29
|
+
"eslint.config.js"
|
|
30
|
+
],
|
|
31
|
+
"exclude": ["node_modules", "dist", "coverage", "build"]
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import react from '@vitejs/plugin-react';
|
|
2
|
+
import tsconfigPaths from 'vite-tsconfig-paths';
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react(), tsconfigPaths()],
|
|
7
|
+
test: {
|
|
8
|
+
globals: true,
|
|
9
|
+
environment: 'jsdom',
|
|
10
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
11
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
12
|
+
exclude: ['node_modules', 'dist', 'build'],
|
|
13
|
+
css: true,
|
|
14
|
+
coverage: {
|
|
15
|
+
provider: 'v8',
|
|
16
|
+
reporter: ['text', 'json', 'html', 'cobertura'],
|
|
17
|
+
include: ['src/**/*.{ts,tsx}'],
|
|
18
|
+
exclude: [
|
|
19
|
+
'src/**/*.test.{ts,tsx}',
|
|
20
|
+
'src/**/*.spec.{ts,tsx}',
|
|
21
|
+
'src/**/*.d.ts',
|
|
22
|
+
'src/test/**',
|
|
23
|
+
'src/mocks/**',
|
|
24
|
+
'src/main.tsx',
|
|
25
|
+
'src/vite-env.d.ts',
|
|
26
|
+
],
|
|
27
|
+
// Thresholds enforced via thresholds block ā vitest v3 no longer supports
|
|
28
|
+
// top-level lines/branches/functions/statements shorthand (deprecated in v2āv3).
|
|
29
|
+
thresholds: {
|
|
30
|
+
lines: 80,
|
|
31
|
+
branches: 75,
|
|
32
|
+
functions: 80,
|
|
33
|
+
statements: 80,
|
|
34
|
+
perFile: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|