diff --git a/PRD.md b/PRD.md index 1cdf77c..50f7a5f 100644 --- a/PRD.md +++ b/PRD.md @@ -306,6 +306,7 @@ Run static analysis, generate project snapshot, and output a readable project ov - Next.js - Express +- React **Responsibilities:** @@ -1330,7 +1331,7 @@ Landing page/web can be added later under `apps/web`, but must not distract from | Risk | Mitigation | |---|---| -| Static analysis inaccurate on unconventional projects | Limit MVP to Next.js + Express only | +| Static analysis inaccurate on unconventional projects | Keep MVP detection scoped to tested Next.js, Express, and standalone React signals | | User frustrated by API key setup | `devmap init` wizard is fully guided | | AI output misleading | Frame output as overview, not absolute ground truth | | Windows / Mac / Linux inconsistency | Test with GitHub Actions matrix | diff --git a/README.md b/README.md index e3f3f76..843f1a5 100644 --- a/README.md +++ b/README.md @@ -246,10 +246,10 @@ Use AI coding assistants to modify it. * Next.js * Express +* React ### Planned -* React * NestJS * Laravel * Nuxt diff --git a/docs/architecture.md b/docs/architecture.md index 8c8466d..12ed998 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -61,8 +61,9 @@ Terminal Output DevMap MVP focuses on: -* Next.js -* Express +* Next.js +* Express +* React Other stacks are future roadmap items. @@ -162,10 +163,16 @@ Framework detection should use: | Signal | Detection | | -------------------------- | ------------------------ | | `next` dependency | Next.js | -| `app/` directory | Next.js App Router | -| `pages/` directory | Next.js Pages Router | -| `express` dependency | Express | -| `server.ts` or `server.js` | Node/Express entry point | +| `app/page`, `app/layout`, or `app/route` | Next.js App Router | +| `pages/_app`, `pages/_document`, or `pages/api` | Next.js Pages Router | +| `express` dependency | Express | +| `server.ts` or `server.js` | Node/Express entry point | +| `react` plus browser runtime/tooling and JSX/TSX source | Standalone React | + +Next.js detection runs before React because Next projects also depend on +React. A generic `src/app/` folder is not enough to infer Next.js; source-only +fallback requires Next conventions such as `app/page`, `app/layout`, +`app/route`, or a Next config file. --- @@ -445,7 +452,7 @@ its `sourcePriority` files before broad repository exploration. The full The index separates technical framework detection from repository shape: ```txt -framework -> nextjs | express | unknown +framework -> nextjs | react | express | unknown projectType -> node-cli | web-app | api-service | library | unknown workspaceType -> monorepo | single-package ``` diff --git a/docs/for-me-personal/PROGRESS.md b/docs/for-me-personal/PROGRESS.md index 250a4f6..fc16ae0 100644 --- a/docs/for-me-personal/PROGRESS.md +++ b/docs/for-me-personal/PROGRESS.md @@ -4,6 +4,18 @@ Terakhir diperbarui: 2026-06-20 ## Update 2026-06-20 +### Standalone React Detection + +- Framework detector sekarang mengenali standalone React dari dependency + `react`, browser runtime/tooling, dan bukti JSX/TSX source. +- Next.js tetap memiliki precedence karena Next juga memakai React. +- Folder generik `src/app/` tidak lagi otomatis dianggap Next.js; fallback + source membutuhkan `app/page`, `app/layout`, `app/route`, atau Next config. +- React peer dependency tanpa runtime app tidak diklasifikasikan sebagai + framework React, sehingga component library tidak menjadi false positive. +- Entry detector sekarang mengenali `main.tsx` sebagai browser entry point. +- Packed-package E2E mencakup fixture React selain Next.js dan Express. + ### Project Classification Dan Start-Here Ranking - Agent index sekarang memisahkan `framework`, `projectType`, dan diff --git a/docs/for-me-personal/TEST.md b/docs/for-me-personal/TEST.md index f4415fe..3de12bb 100644 --- a/docs/for-me-personal/TEST.md +++ b/docs/for-me-personal/TEST.md @@ -436,6 +436,7 @@ Fixture aman digunakan karena tidak mengubah project pribadi: ```powershell pnpm dev:cli -- analyze packages/cli/test/fixtures/nextjs-project --fresh pnpm dev:cli -- analyze packages/cli/test/fixtures/express-project --fresh +pnpm dev:cli -- analyze packages/cli/test/fixtures/react-project --fresh ``` Hasil penting Next.js: @@ -448,6 +449,14 @@ Hasil penting Next.js: Hasil penting Express: - framework `express`; + +Expected React fixture: + +- framework `react`; +- project type `web-app`; +- entry point `src/main.tsx`; +- tidak menghasilkan route palsu hanya karena memakai React; +- package dengan React peer dependency saja tetap `unknown`. - entry point `src/server.ts`; - route payment dan Stripe terdeteksi. diff --git a/docs/generated-files.md b/docs/generated-files.md index d1dd3e8..7e5521c 100644 --- a/docs/generated-files.md +++ b/docs/generated-files.md @@ -105,7 +105,7 @@ The snapshot contains: - `version` and `generatedAt` - `fingerprint` for stale snapshot detection -- project name, root, framework, language, and package manager +- project name, root, framework, project/workspace type, language, and package manager - file and line statistics - entry points and scored critical files with reasons - page routes and API routes diff --git a/packages/cli/src/analyzers/entryPoints.ts b/packages/cli/src/analyzers/entryPoints.ts index 566f10d..8fe8514 100644 --- a/packages/cli/src/analyzers/entryPoints.ts +++ b/packages/cli/src/analyzers/entryPoints.ts @@ -5,7 +5,7 @@ const ENTRY_PATTERNS = [ /(^|\/)page\.[jt]sx?$/, /(^|\/)layout\.[jt]sx?$/, /(^|\/)middleware\.[jt]s$/, - /(^|\/)(server|app|index|main)\.[cm]?[jt]s$/, + /(^|\/)(server|app|index|main)\.[cm]?[jt]sx?$/, /(^|\/)route\.[jt]s$/ ]; diff --git a/packages/cli/src/analyzers/frameworkDetector.ts b/packages/cli/src/analyzers/frameworkDetector.ts index dde5e93..7a34d12 100644 --- a/packages/cli/src/analyzers/frameworkDetector.ts +++ b/packages/cli/src/analyzers/frameworkDetector.ts @@ -1,16 +1,19 @@ import type { ScannedFile } from "./fileScanner.js"; import { isArchitectureSource } from "./sourceScope.js"; -export type Framework = "nextjs" | "express" | "unknown"; +export type Framework = "nextjs" | "react" | "express" | "unknown"; export function detectFramework(files: ScannedFile[]): Framework { - const packageJson = files.find((file) => file.path === "package.json"); - const dependencies = packageJson ? readDependencies(packageJson.content) : {}; + const dependencies = readAllDependencies(files); const sourceFiles = files.filter((file) => isArchitectureSource(file.path)); if ( "next" in dependencies - || sourceFiles.some((file) => /^(?:src\/)?(?:app|pages)\//.test(file.path)) + || files.some((file) => /^next\.config\.[cm]?[jt]s$/.test(file.path)) + || sourceFiles.some((file) => + /^(?:src\/)?app\/(?:.+\/)?(?:page|layout|route)\.[jt]sx?$/.test(file.path) + || /^(?:src\/)?pages\/(?:_app|_document|api\/)/.test(file.path) + ) ) { return "nextjs"; } @@ -22,9 +25,31 @@ export function detectFramework(files: ScannedFile[]): Framework { return "express"; } + const hasReactRuntime = "react-dom" in dependencies + || "react-scripts" in dependencies + || "@vitejs/plugin-react" in dependencies + || "@vitejs/plugin-react-swc" in dependencies; + const hasReactSource = sourceFiles.some((file) => + /\.[jt]sx$/.test(file.path) + || /(?:from\s+["']react["']|from\s+["']react-dom(?:\/client)?["']|require\(["']react["']\))/.test(file.content) + ); + + if ("react" in dependencies && hasReactRuntime && hasReactSource) { + return "react"; + } + return "unknown"; } +function readAllDependencies(files: ScannedFile[]): Record { + return files + .filter((file) => file.path.endsWith("package.json")) + .reduce((dependencies, file) => ({ + ...dependencies, + ...readDependencies(file.content) + }), {} as Record); +} + function readDependencies(content: string): Record { try { const parsed = JSON.parse(content) as { diff --git a/packages/cli/src/analyzers/projectMetadata.ts b/packages/cli/src/analyzers/projectMetadata.ts index 43b58b9..220adcd 100644 --- a/packages/cli/src/analyzers/projectMetadata.ts +++ b/packages/cli/src/analyzers/projectMetadata.ts @@ -85,7 +85,9 @@ function detectProjectType( manifests: PackageManifest[] ): ProjectType { if (manifests.some((manifest) => manifest.bin)) return "node-cli"; - if (framework === "nextjs" || hasDependency(manifests, "astro")) return "web-app"; + if (["nextjs", "react"].includes(framework) || hasDependency(manifests, "astro")) { + return "web-app"; + } if (framework === "express") return "api-service"; if (manifests.some((manifest) => manifest.exports || manifest.main)) return "library"; return "unknown"; diff --git a/packages/cli/src/cache/snapshot.ts b/packages/cli/src/cache/snapshot.ts index 5b20925..48606fb 100644 --- a/packages/cli/src/cache/snapshot.ts +++ b/packages/cli/src/cache/snapshot.ts @@ -142,7 +142,7 @@ function normalizeSnapshotDefaults(snapshot: Record): void { if (isRecord(snapshot.project)) { if (typeof snapshot.project.projectType !== "string") { - snapshot.project.projectType = snapshot.project.framework === "nextjs" + snapshot.project.projectType = ["nextjs", "react"].includes(String(snapshot.project.framework)) ? "web-app" : snapshot.project.framework === "express" ? "api-service" diff --git a/packages/cli/test/analyzers.test.ts b/packages/cli/test/analyzers.test.ts index c52123d..174bf52 100644 --- a/packages/cli/test/analyzers.test.ts +++ b/packages/cli/test/analyzers.test.ts @@ -77,6 +77,76 @@ test("framework detector recognizes Next.js and Express fixtures", async () => { assert.equal(detectFramework(expressFiles), "express"); }); +test("framework detector recognizes standalone React without downgrading Next.js", () => { + const reactFiles = [ + createScannedFile("package.json", JSON.stringify({ + dependencies: { + react: "^19.0.0", + "react-dom": "^19.0.0" + }, + devDependencies: { + "@vitejs/plugin-react": "^5.0.0", + vite: "^7.0.0" + } + })), + createScannedFile( + "src/main.tsx", + 'import { createRoot } from "react-dom/client";\ncreateRoot(document.body).render();\n' + ), + createScannedFile( + "src/app/Shell.tsx", + "export function Shell() { return
; }\n" + ) + ]; + const reactLibraryFiles = [ + createScannedFile("package.json", JSON.stringify({ + peerDependencies: { react: "^19.0.0" } + })), + createScannedFile("src/index.ts", "export const version = '1';\n") + ]; + + assert.equal(detectFramework(reactFiles), "react"); + assert.equal(detectFramework(reactLibraryFiles), "unknown"); +}); + +test("project map classifies a standalone React app and finds its browser entry", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "devmap-react-project-")); + + try { + await mkdir(join(projectRoot, "src"), { recursive: true }); + await writeFile(join(projectRoot, "package.json"), JSON.stringify({ + name: "react-fixture", + description: "A standalone React dashboard.", + dependencies: { + react: "^19.0.0", + "react-dom": "^19.0.0" + }, + devDependencies: { + "@vitejs/plugin-react": "^5.0.0", + vite: "^7.0.0", + typescript: "^6.0.0" + } + }), "utf8"); + await writeFile( + join(projectRoot, "src", "main.tsx"), + 'import { createRoot } from "react-dom/client";\nimport { App } from "./App.js";\ncreateRoot(document.body).render();\n', + "utf8" + ); + await writeFile( + join(projectRoot, "src", "App.tsx"), + "export function App() { return
Dashboard
; }\n", + "utf8" + ); + + const projectMap = await createProjectMap(projectRoot); + assert.equal(projectMap.framework, "react"); + assert.equal(projectMap.project.projectType, "web-app"); + assert.ok(projectMap.entryPoints.includes("src/main.tsx")); + } finally { + await rm(projectRoot, { recursive: true, force: true }); + } +}); + test("dependency graph resolves TypeScript imports using .js specifiers", async () => { const files = await scanFiles(nextFixture); const graph = buildDependencyGraph(files); diff --git a/packages/cli/test/fixtures/react-project/package.json b/packages/cli/test/fixtures/react-project/package.json new file mode 100644 index 0000000..df13348 --- /dev/null +++ b/packages/cli/test/fixtures/react-project/package.json @@ -0,0 +1,12 @@ +{ + "name": "react-fixture", + "private": true, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^5.0.0", + "vite": "^7.0.0" + } +} diff --git a/packages/cli/test/fixtures/react-project/src/App.tsx b/packages/cli/test/fixtures/react-project/src/App.tsx new file mode 100644 index 0000000..2834bf8 --- /dev/null +++ b/packages/cli/test/fixtures/react-project/src/App.tsx @@ -0,0 +1,3 @@ +export function App() { + return
React fixture
; +} diff --git a/packages/cli/test/fixtures/react-project/src/main.tsx b/packages/cli/test/fixtures/react-project/src/main.tsx new file mode 100644 index 0000000..0ff378c --- /dev/null +++ b/packages/cli/test/fixtures/react-project/src/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from "react-dom/client"; +import { App } from "./App.js"; + +createRoot(document.body).render(); diff --git a/packages/cli/test/package-e2e.mjs b/packages/cli/test/package-e2e.mjs index 16a746e..79e6490 100644 --- a/packages/cli/test/package-e2e.mjs +++ b/packages/cli/test/package-e2e.mjs @@ -52,8 +52,9 @@ try { const tarballPath = join(artifactsDirectory, tarballName); await verifyProject("nextjs-project", "nextjs"); await verifyProject("express-project", "express"); + await verifyProject("react-project", "react"); - console.log("Packed CLI E2E passed for Next.js and Express fixtures."); + console.log("Packed CLI E2E passed for Next.js, Express, and React fixtures."); async function verifyProject(fixtureName, expectedFramework) { const projectRoot = join(temporaryRoot, fixtureName);