import { describe, it, expect } from 'vitest' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' /** * Bundle isolation: the store apps (store-server / pos / backoffice) and the HQ * apps (hq / hq-web) are separate deliverables that must never import each other. * A static, dependency-free scan of their src/ trees enforces the wall — shared * @sims/* packages are the only sanctioned bridge. See docs/15 (D15 one-repo/two-apps). */ const APPS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..') const STORE_APPS = ['store-server', 'pos', 'backoffice'] const HQ_APPS = ['hq', 'hq-web'] /** Every .ts/.tsx file under apps//src, recursively. */ function sourceFiles(app: string): string[] { const root = path.join(APPS_DIR, app, 'src') const out: string[] = [] const walk = (dir: string): void => { if (!fs.existsSync(dir)) return for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name) if (entry.isDirectory()) walk(full) else if (/\.tsx?$/.test(entry.name)) out.push(full) } } walk(root) return out } /** Static import / re-export / dynamic-import / require specifiers in a source file. */ function importSpecifiers(file: string): string[] { const src = fs.readFileSync(file, 'utf8') const re = /\b(?:from|import|require)\b\s*\(?\s*['"]([^'"]+)['"]/g const specs: string[] = [] let m: RegExpExecArray | null while ((m = re.exec(src)) !== null) specs.push(m[1]!) return specs } /** Does `spec`, imported from `file`, reach into one of the forbidden app trees? */ function crossesInto(file: string, spec: string, forbiddenDirs: string[], forbiddenNames: string[]): boolean { if (spec.startsWith('.')) { const resolved = path.resolve(path.dirname(file), spec) return forbiddenDirs.some((d) => resolved === d || resolved.startsWith(d + path.sep)) } // Bare specifiers: @sims/* and third-party deps are fine; an explicit apps/ path is not. return forbiddenNames.some((name) => spec.includes(`apps/${name}`) || spec.includes(`apps\\${name}`)) } function violations(fromApps: string[], toApps: string[]): string[] { const forbiddenDirs = toApps.map((a) => path.join(APPS_DIR, a)) const found: string[] = [] for (const app of fromApps) { for (const file of sourceFiles(app)) { for (const spec of importSpecifiers(file)) { if (crossesInto(file, spec, forbiddenDirs, toApps)) { found.push(`${path.relative(APPS_DIR, file)} imports '${spec}'`) } } } } return found } describe('bundle isolation between store apps and HQ apps', () => { it('scans a non-empty set of source files (guards against a broken path)', () => { expect([...STORE_APPS, ...HQ_APPS].flatMap(sourceFiles).length).toBeGreaterThan(0) }) it('store apps never import HQ apps', () => { expect(violations(STORE_APPS, HQ_APPS)).toEqual([]) }) it('HQ apps never import store apps', () => { expect(violations(HQ_APPS, STORE_APPS)).toEqual([]) }) })