chore: enforce domain folder naming
This commit is contained in:
parent
9be159de3d
commit
3e6093106c
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue