chore: enforce domain folder naming

This commit is contained in:
Danila Poyarkov 2026-05-14 20:57:19 +03:00
parent 9be159de3d
commit 3e6093106c
3 changed files with 49 additions and 1 deletions

View file

@ -251,7 +251,7 @@ OpenPencil follows a Reka UI-inspired component namespace structure:
- Non-component domain folders use lowercase or kebab-case: `scene-graph/`, `figma-api/`, `node-edit/`.
- Non-component TypeScript files use lowercase or kebab-case unless they are conventional entrypoints such as `index.ts`, `types.ts`, `context.ts`, or `use.ts`.
- Multi-file root components live inside their component namespace folder, not beside it.
- Use subfolders for multi-file domains instead of sibling files with repeated prefixes. Prefer `selection/container.ts`, `selection/hit-test.ts` over `selection-container.ts`, `selection-hit-test.ts`. When adding a second file for a domain (e.g. `eval-wrap.ts` next to `eval.ts`), create the folder immediately (`eval/index.ts` + `eval/wrap.ts`) instead of prefixing. The lint rule `no-sibling-domain-prefixed-files` catches this when a sibling folder exists, but the convention applies even before the folder is created.
- Use subfolders for multi-file domains instead of sibling files with repeated prefixes. Prefer `selection/container.ts`, `selection/hit-test.ts` over `selection-container.ts`, `selection-hit-test.ts`. When adding a second file for a domain (e.g. `eval-wrap.ts` next to `eval.ts`), create the folder immediately (`eval/index.ts` + `eval/wrap.ts`) instead of prefixing. Oxlint catches sibling prefix files when a sibling folder exists; Steiger catches 3+ sibling files with the same prefix. The convention applies even before either rule triggers.
- `@/` import alias for app cross-directory imports; app feature code lives under `src/app/*`
- Use package-local aliases inside workspace packages: `#vue/*` in `packages/vue`, `#cli/*` in `packages/cli`, `#mcp/*` in `packages/mcp`, and `#core/*` when core code needs an alias. Prefer relative imports within nearby core modules when that is clearer than an alias.

View file

@ -17,6 +17,11 @@ type Rule = { name: string; check: (root: TreeEntry) => RuleResult }
type FileRuleCheck = (sourceRel: string) => string | null
const FILE_PREFIX_GROUP_ALLOWLIST = new Set([
'packages/core/src/lint/rules::no',
'tests/engine::visual'
])
type ImportRef = {
specifier: string
line: number
@ -62,6 +67,13 @@ function collectFiles(entry: TreeEntry, files: string[] = []) {
return files
}
function collectFolders(entry: TreeEntry, folders: TreeEntry[] = []) {
if (entry.type !== 'folder') return folders
folders.push(entry)
for (const child of entry.children ?? []) collectFolders(child, folders)
return folders
}
function importsIn(content: string): ImportRef[] {
const imports: ImportRef[] = []
const patterns = [
@ -139,6 +151,40 @@ function createImportRule(
}
}
function filePrefix(filePath: string): string | null {
const name = path.basename(filePath).replace(/\.(test|spec|bench)?\.?[cm]?[tj]sx?$|\.vue$/, '')
const match = /^([a-z][a-z0-9]+)-[a-z0-9-]+$/.exec(name)
return match?.[1] ?? null
}
const preferDomainFoldersOverFilenamePrefixes: Rule = {
name: 'open-pencil/prefer-domain-folders-over-filename-prefixes',
check(root) {
const diagnostics: Diagnostic[] = []
for (const folder of collectFolders(root)) {
const folderRel = relativePath(root.path, folder.path)
const groups = new Map<string, string[]>()
for (const child of folder.children ?? []) {
if (child.type !== 'file' || !TEXT_EXTENSIONS.has(path.extname(child.path))) continue
const prefix = filePrefix(child.path)
if (!prefix) continue
const files = groups.get(prefix) ?? []
files.push(child.path)
groups.set(prefix, files)
}
for (const [prefix, files] of groups) {
if (files.length < 3) continue
if (FILE_PREFIX_GROUP_ALLOWLIST.has(`${folderRel}::${prefix}`)) continue
diagnostics.push({
message: `Use a ${prefix}/ domain folder instead of ${files.length} sibling files with the ${prefix}- filename prefix.`,
location: { path: folder.path }
})
}
}
return { diagnostics }
}
}
const strictTestFilePlacement = createFileRule('open-pencil/strict-test-file-placement', (sourceRel) => {
if (!sourceRel.startsWith('tests/')) return null
if (!TEXT_EXTENSIONS.has(path.extname(sourceRel))) return null
@ -350,6 +396,7 @@ const noUiImportsInCore = createImportRule(
export const openPencilArchitecturePlugin = {
meta: { name: 'open-pencil-architecture', version: '0.0.0' },
ruleDefinitions: [
preferDomainFoldersOverFilenamePrefixes,
strictTestFilePlacement,
noEngineOnlyAssertionsInE2E,
noE2EImportsInEngineTests,

View file

@ -21,6 +21,7 @@ export default defineConfig([
},
{
rules: {
'open-pencil/prefer-domain-folders-over-filename-prefixes': 'error',
'open-pencil/strict-test-file-placement': 'error',
'open-pencil/no-engine-only-assertions-in-e2e': 'error',
'open-pencil/no-e2e-imports-in-engine-tests': 'error',