@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.
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ echo "šŸ“ Validating commit message..."
5
+ pnpm exec commitlint --edit "$1"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ echo "šŸ” Running lint-staged..."
5
+ pnpm exec lint-staged
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ echo "šŸš€ Running pre-push quality checks..."
5
+ pnpm lint && pnpm check-modularization && pnpm check-types && pnpm test:ci
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ dist
3
+ .output
4
+ .vinxi
5
+ build
6
+ coverage
7
+ pnpm-lock.yaml
8
+ package-lock.json
9
+ bun.lockb
10
+ *.gen.ts
11
+ routeTree.gen.ts
@@ -0,0 +1,9 @@
1
+ {
2
+ "recommendations": [
3
+ "dbaeumer.vscode-eslint",
4
+ "esbenp.prettier-vscode",
5
+ "bradlc.vscode-tailwindcss",
6
+ "christian-kohler.npm-intellisense",
7
+ "christian-kohler.path-intellisense"
8
+ ]
9
+ }
@@ -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
+ };
@@ -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,12 @@
1
+ export default {
2
+ semi: true,
3
+ singleQuote: true,
4
+ tabWidth: 2,
5
+ useTabs: false,
6
+ trailingComma: 'es5',
7
+ printWidth: 100,
8
+ bracketSpacing: true,
9
+ arrowParens: 'always',
10
+ endOfLine: 'lf',
11
+ plugins: ['prettier-plugin-organize-imports'],
12
+ };
@@ -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
+ });