mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:01:10 +00:00
[AZ-485] Add Public API barrels + STC-ARCH-01 (F4 close)
Closes architecture baseline finding F4. Every component now exposes its Public API through `src/<component>/index.ts`; cross-component imports go through the barrel. `scripts/check-arch-imports.mjs` plus `STC-ARCH-01` in the static profile enforce the rule; tests in `tests/architecture_imports.test.ts` cover AC-4/AC-5 + 2 exemption cases. One F3-pending exemption (`classColors`) is documented in 5 places (barrel, consumer, script, doc, test) to avoid a circular import. Phase B cycle 1 batch 1 of 2 (epic AZ-447). Batch 2 is AZ-486 (endpoint builders) — blocked on this commit landing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env node
|
||||
// AZ-485 / F4 — STC-ARCH-01: no cross-component deep imports.
|
||||
//
|
||||
// Every component under src/<component>/ exposes its Public API via a barrel
|
||||
// (src/<component>/index.ts). Cross-component imports MUST go through the
|
||||
// barrel; reaching into another component's internal files is a layering
|
||||
// violation.
|
||||
//
|
||||
// Single source of truth — scripts/run-tests.sh delegates here, and the
|
||||
// architecture unit test calls this script with a synthetic fixture to verify
|
||||
// the check fails on a deep import (AC-4) and passes on the migrated codebase
|
||||
// (AC-5). Mirrors the scripts/check-banned-deps.mjs pattern (AZ-482).
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/check-arch-imports.mjs [--root=<repo-root>]
|
||||
//
|
||||
// Exit code 0 on PASS (no offending deep imports); non-zero on FAIL with the
|
||||
// hit list on stderr.
|
||||
|
||||
import { readFileSync, statSync, readdirSync } from 'node:fs'
|
||||
import { join, dirname, resolve, relative } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { root: resolve(__dirname, '..') }
|
||||
for (const a of argv.slice(2)) {
|
||||
if (a.startsWith('--root=')) out.root = resolve(a.slice('--root='.length))
|
||||
else if (a === '-h' || a === '--help') {
|
||||
process.stderr.write('Usage: check-arch-imports.mjs [--root=<repo-root>]\n')
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const SCAN_ROOTS = ['src', 'tests', 'e2e']
|
||||
const IGNORED_DIRS = new Set([
|
||||
'node_modules', 'dist', 'build', 'test-output', 'test-results',
|
||||
'coverage', '.git', '.cache', 'playwright-report', 'blob-report',
|
||||
])
|
||||
const SOURCE_EXT = new Set(['.ts', '.tsx'])
|
||||
|
||||
// Cross-component deep-import pattern: `from '<up>/<src>?/<component>/<File>'`
|
||||
// - one or more `..` segments
|
||||
// - optional `src/` prefix (used by tests/ and e2e/ imports)
|
||||
// - a known component directory
|
||||
// - a path segment starting with a letter (i.e. NOT the barrel `index`)
|
||||
//
|
||||
// Allowed by construction:
|
||||
// - barrel: from '../api' (no further /<File>)
|
||||
// - intra-component: from './sse' (starts with ./, not ../)
|
||||
const COMPONENT_DIRS = 'api|auth|components|features/[a-z-]+|hooks|i18n'
|
||||
const DEEP_IMPORT_RE = new RegExp(
|
||||
String.raw`from\s+['"](?:\.\./)+(?:src/)?(?:${COMPONENT_DIRS})/[A-Za-z]`,
|
||||
)
|
||||
|
||||
// F3-pending exemptions:
|
||||
// - `features/annotations/classColors` — classColors is logically owned by
|
||||
// 11_class-colors but physically lives under 06_annotations. Re-exporting
|
||||
// it through the 06_annotations barrel creates a circular import:
|
||||
// AnnotationsPage -> DetectionClasses -> 06_annotations barrel
|
||||
// -> AnnotationsPage
|
||||
// so consumers (DetectionClasses, tests/detection_classes.test.tsx)
|
||||
// import the file directly. F3 will move the file and remove this
|
||||
// exemption.
|
||||
const EXEMPT_RE = /features\/annotations\/classColors/
|
||||
|
||||
function* walkSourceFiles(rootDir) {
|
||||
let entries
|
||||
try {
|
||||
entries = readdirSync(rootDir, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = join(rootDir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (IGNORED_DIRS.has(entry.name)) continue
|
||||
yield* walkSourceFiles(full)
|
||||
} else if (entry.isFile()) {
|
||||
const dot = entry.name.lastIndexOf('.')
|
||||
if (dot < 0) continue
|
||||
const ext = entry.name.slice(dot)
|
||||
if (!SOURCE_EXT.has(ext)) continue
|
||||
yield full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scanFile(file, root) {
|
||||
const hits = []
|
||||
let text
|
||||
try {
|
||||
text = readFileSync(file, 'utf8')
|
||||
} catch {
|
||||
return hits
|
||||
}
|
||||
const rel = relative(root, file).replaceAll('\\', '/')
|
||||
const lines = text.split('\n')
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (/^\s*\/\//.test(line)) continue
|
||||
if (!DEEP_IMPORT_RE.test(line)) continue
|
||||
if (EXEMPT_RE.test(line)) continue
|
||||
hits.push(`${rel}:${i + 1}: ${line.trim().slice(0, 200)}`)
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { root } = parseArgs(process.argv)
|
||||
const hits = []
|
||||
for (const sub of SCAN_ROOTS) {
|
||||
const full = join(root, sub)
|
||||
try { statSync(full) } catch { continue }
|
||||
for (const file of walkSourceFiles(full)) {
|
||||
hits.push(...scanFile(file, root))
|
||||
}
|
||||
}
|
||||
if (hits.length) {
|
||||
process.stderr.write(
|
||||
'STC-ARCH-01 — cross-component deep imports detected ' +
|
||||
'(must go through component barrel, see module-layout.md):\n',
|
||||
)
|
||||
for (const h of hits) process.stderr.write(` ${h}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -470,6 +470,24 @@ if [ "$RUN_STATIC" = "true" ]; then
|
||||
'
|
||||
}
|
||||
|
||||
# AZ-485 F4 — STC-ARCH-01: no cross-component deep imports. After F4 every
|
||||
# component exposes its Public API via `src/<component>/index.ts`; cross-
|
||||
# component imports MUST go through the barrel, not reach into another
|
||||
# component's internal files. Flags imports of the form
|
||||
# from '../api/client'
|
||||
# from '../../components/ConfirmDialog'
|
||||
# from '../src/features/annotations/AnnotationsPage' (test files)
|
||||
# Allowed:
|
||||
# - barrel imports: from '../api', from '../../components'
|
||||
# - intra-component: from './sse', from './MediaList' (./ not ..)
|
||||
# - F3-pending edge: from '../features/annotations/classColors'
|
||||
# (classColors lives under 06_annotations until F3 moves it; importing
|
||||
# through the 06_annotations barrel would create a circular import
|
||||
# AnnotationsPage → DetectionClasses → barrel → AnnotationsPage.)
|
||||
static_check_no_cross_component_deep_imports() {
|
||||
node "$PROJECT_ROOT/scripts/check-arch-imports.mjs"
|
||||
}
|
||||
|
||||
# AZ-479 NFT-PERF-01 / NFT-RES-LIM-01 — initial JS bundle ≤ 2 MB gzipped.
|
||||
# Same threshold + measurement as scripts/run-performance-tests.sh; this
|
||||
# entry routes the gate through the static profile so every commit is
|
||||
@@ -516,6 +534,7 @@ if [ "$RUN_STATIC" = "true" ]; then
|
||||
run_static "STC-T1" "tsc --noEmit (test config)" "AC-6" "n/a" static_check_typecheck
|
||||
run_static "STC-B1" "vite build succeeds" "AC-6" "n/a" static_check_vite_build
|
||||
run_static "STC-S5" "mission-planner not in dist/" "AC-31" "n/a" static_check_dist_no_mission_planner
|
||||
run_static "STC-ARCH-01" "no cross-component deep imports (barrel-only)" "F4" "AZ-485" static_check_no_cross_component_deep_imports
|
||||
run_static "STC-PERF01" "initial JS bundle ≤ 2 MB gz" "NFT-PERF-01" "40" static_check_bundle_size
|
||||
run_static "STC-RES02" "nginx client_max_body_size 500M" "NFT-RES-LIM-02" "n/a" static_check_nginx_body_cap
|
||||
run_static "STC-RES03" "Dockerfile final stage nginx:alpine no Node" "NFT-RES-LIM-03" "n/a" static_check_dockerfile_nginx_alpine
|
||||
|
||||
Reference in New Issue
Block a user