@devmedic/project-scanner
v0.1.0
Published
Fast, parallel, cached detection of project structure and platform (React Native, Expo, TypeScript/JavaScript) — returns a ProjectContext.
Readme
@devmedic/project-scanner
Fast, parallel, cached detection of what a project actually is — React
Native, Expo, TypeScript, JavaScript — from its structure. scanProject(root)
returns a ProjectContext describing that structure; nothing here executes
rules or reads file contents beyond package.json.
import { scanProject } from '@devmedic/project-scanner';
const context = await scanProject(process.cwd());
context.platform.isReactNative; // boolean
context.platform.isExpo; // boolean
context.platform.isReact; // boolean
context.platform.isNode; // boolean
context.platform.isExpress; // boolean
context.platform.isNestJs; // boolean
context.platform.isNextJs; // boolean
context.language.detected; // 'typescript' | 'javascript' | 'mixed' | 'unknown'
context.packageJson?.workspaces; // readonly string[] | null — the "workspaces" field, if any
context.packageJson?.hasBin; // boolean — whether a "bin" field is present
context.packageJson?.main; // string | null — the "main" field, if anyWhat it reads
| Signal | Read as |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| package.json | Parsed for name, version, dependencies, devDependencies, workspaces, bin, main |
| android/, ios/, src/, assets/ | Existence only (no contents) |
| metro.config.{js,cjs,mjs,ts} | First match, in that priority order |
| babel.config.{js,cjs,mjs,json}, .babelrc[.js\|.json] | First match, in that priority order |
| android/**/build.gradle[.kts], settings.gradle[.kts] | Every match found |
| ios/Podfile | Existence |
| tsconfig.json + **/*.{ts,tsx,js,jsx} file counts | Language classification |
The **/*.{ts,tsx,js,jsx} count excludes node_modules, dist, build,
.expo, android/build, ios/Pods, ios/build, and root-level tooling
config (*.config.*, .babelrc*) — a metro.config.js doesn't make a
project "JavaScript."
Detection rules
- React Native: a
react-nativedependency, or ametro.config.*file, or bothandroid/andios/existing together (neither alone is a signal). - Expo: an
expoorexpo-modules-coredependency. - React:
isReactNative, or areactdependency. - Next.js: a
nextdependency (implies Node.js). - Express: an
expressdependency (implies Node.js). - NestJS: an
@nestjs/coredependency (implies Node.js). - Node.js: any of the above, or an
@types/nodedependency. - Language:
tsconfig.jsonpresent, or.ts/.tsxfiles with no.js/.jsx→"typescript"..js/.jsxwith no TS →"javascript". Both, notsconfig.json→"mixed". Neither →"unknown".
@devmedic/project-detection-engine builds on these signals — plus workspaces/bin/main for monorepo/CLI/library detection — to decide whether a given file even belongs to the project a rule targets. This package stays framework-detection-only; it has no notion of "rules" or "gating."
Performance
- Parallel: every read for a fresh scan (
package.json, directory structure, build files, language file counts) runs concurrently viaPromise.all. - Cached: results are memoized in-process, keyed by resolved root path. A repeat
scanProject(root)call costs a singlestatonpackage.jsonto confirm nothing changed, then returns the cachedProjectContextinstantly (fromCache: true). - Auto-invalidating: if
package.json's mtime has changed since it was cached, the cache is bypassed and a fresh scan runs — catches the common case (a dependency added/removed) without a manual cache-clear. It does not catch a purely structural change (e.g. a newandroid/folder) that leavespackage.jsonuntouched; callclearProjectScannerCache(root)or passscanProject(root, { cache: false })for that.
Depends on
fast-glob
