feat(hq): HQ-1.5 — module quote content, pack pick-lists, bundle isolation check
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>feat/client-detail-redesign
parent
8c145d5a3b
commit
54d6a63e81
@ -0,0 +1,79 @@
|
||||
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/<name>/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/<other> 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([])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue