@devmedic/project-detection-engine
v0.1.0
Published
Detects which project(s) a file belongs to (React Native, React, Node.js, Express, NestJS, Expo, Next.js, Monorepo, Library, CLI) and gates rule execution so a rule never runs against a file outside its declared project type.
Readme
@devmedic/project-detection-engine
Decides whether a file even belongs to the project a rule targets — so a rule declared for React Native never runs against a CLI package, a parser package, a fixture, or any other file it was never meant to see, even though every rule and every file live in the same monorepo.
import { detectProjectTypes, createProjectDetectionGate } from '@devmedic/project-detection-engine';
const detection = await detectProjectTypes('/path/to/some/project');
detection.projectTypes; // e.g. ['react-native', 'expo']
detection.platforms; // e.g. ['ios', 'android']
detection.isMonorepoRoot; // boolean
detection.isNestedInMonorepo; // boolean
const gate = createProjectDetectionGate();
await gate.shouldRun(rule, '/path/to/some/project/src/App.tsx'); // booleancreateProjectDetectionGate()'s result is passed as
@devmedic/rule-engine#AnalyzeOptions.projectGate — see that package's
README, "Gating rule execution by project," for the engine-side contract.
This package has no dependency on @devmedic/rule-engine either way: the
gate is accepted structurally, by shape, not by type import.
What it detects
| Type | Signal |
| -------------- | ------------------------------------------------------------------------- |
| react-native | @devmedic/project-scanner's platform.isReactNative |
| expo | platform.isExpo |
| react | platform.isReact (React Native implies React too) |
| nextjs | platform.isNextJs |
| express | platform.isExpress |
| nestjs | platform.isNestJs |
| node | platform.isNode (true for Next.js/Express/NestJS/@types/node/CLI too) |
| monorepo | package.json's workspaces field, or a pnpm-workspace.yaml file |
| library | package.json has a main field and no bin field |
| cli | package.json has a bin field |
Platforms detected: ios, android (React Native/Expo, or android/+ios/
directories), web (plain React or Next.js), node (Node.js/Express/NestJS/
CLI).
A project can be several types at once — e.g. a Next.js app is also
react; a monorepo root that ships a binary is also cli. Detection never
hardcodes a hierarchy between them; it derives every applicable type
independently from the same underlying signals.
Ignored paths
Never detected, never gated in — always denied outright, regardless of what a rule declares:
node_modules dist build coverage .tmp .cache
fixtures __fixtures__ tests __tests__ spec examples
generated storybook ios/build android/build Pods DerivedDataMatched by path segment (isIgnoredPath), not glob syntax — ios/build
requires those two segments consecutively; a directory named
node_modules_backup doesn't match node_modules. Pass
{ extraIgnorePatterns: [...] } to createProjectDetectionGate to extend
(not replace) this list.
For real gitignore-syntax matching (**, !negation, character classes),
pass { isIgnored: (path) => boolean } instead — consulted in addition to
the built-in check, never instead of it. This is the hook
@devmedic/ignore-engine#IgnoreService.isIgnored plugs into directly, so
the gate agrees with whatever .gitignore/.devmedicignore/workspace/
plugin/CLI ignores a caller has already merged, without this package
taking a dependency on that one:
import { createIgnoreService } from '@devmedic/ignore-engine';
const ignoreService = await createIgnoreService({ projectRoot });
const gate = createProjectDetectionGate({ isIgnored: (path) => ignoreService.isIgnored(path) });Nested projects
Detection walks every ancestor package.json, not just the nearest one — a
package nested inside a monorepo is detected using its own signals
(isNestedInMonorepo: true, but isMonorepoRoot: false), while the
workspace root itself gets isMonorepoRoot: true and the monorepo project
type. A rule scoped to ['react-native'] still matches a React Native
package nested three levels deep — monorepo is an orthogonal fact about a
specific root, not a prefix every nested project inherits.
Performance
Both the ancestor-root filesystem walk (resolveProjectRootChain) and the
detection itself (detectProjectTypes) are cached in-process — keyed by
directory and by resolved project root, respectively. Analyzing many files
in the same project costs one real detection, not one per file. Clear
either cache (clearProjectRootCache/clearProjectTypeDetectionCache) if a
long-lived process needs to react to a filesystem change.
Depends on
@devmedic/project-scanner— the underlyingscanProject/readPackageJsonSummarythis package buildsmonorepo/library/clidetection and the ignore/gate logic on top of.
