Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ Run static analysis, generate project snapshot, and output a readable project ov

- Next.js
- Express
- React

**Responsibilities:**

Expand Down Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,10 @@ Use AI coding assistants to modify it.

* Next.js
* Express
* React

### Planned

* React
* NestJS
* Laravel
* Nuxt
Expand Down
21 changes: 14 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ Terminal Output

DevMap MVP focuses on:

* Next.js
* Express
* Next.js
* Express
* React

Other stacks are future roadmap items.

Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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
```
Expand Down
12 changes: 12 additions & 0 deletions docs/for-me-personal/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/for-me-personal/TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/generated-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/analyzers/entryPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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$/
];

Expand Down
33 changes: 29 additions & 4 deletions packages/cli/src/analyzers/frameworkDetector.ts
Original file line number Diff line number Diff line change
@@ -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";
}
Expand All @@ -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<string, string> {
return files
.filter((file) => file.path.endsWith("package.json"))
.reduce((dependencies, file) => ({
...dependencies,
...readDependencies(file.content)
}), {} as Record<string, string>);
}

function readDependencies(content: string): Record<string, string> {
try {
const parsed = JSON.parse(content) as {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/analyzers/projectMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cache/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ function normalizeSnapshotDefaults(snapshot: Record<string, unknown>): 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"
Expand Down
70 changes: 70 additions & 0 deletions packages/cli/test/analyzers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(<App />);\n'
),
createScannedFile(
"src/app/Shell.tsx",
"export function Shell() { return <main />; }\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(<App />);\n',
"utf8"
);
await writeFile(
join(projectRoot, "src", "App.tsx"),
"export function App() { return <main>Dashboard</main>; }\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);
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/test/fixtures/react-project/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions packages/cli/test/fixtures/react-project/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function App() {
return <main>React fixture</main>;
}
4 changes: 4 additions & 0 deletions packages/cli/test/fixtures/react-project/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createRoot } from "react-dom/client";
import { App } from "./App.js";

createRoot(document.body).render(<App />);
3 changes: 2 additions & 1 deletion packages/cli/test/package-e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading