stackpatch 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/stackpatch.js +182 -43
  2. package/package.json +2 -1
@@ -841,55 +841,70 @@ function getParentDirectories(filePath, rootPath) {
841
841
  }
842
842
  return dirs;
843
843
  }
844
+ /**
845
+ * Check if a directory is a Next.js app
846
+ */
847
+ function isNextJsApp(dir) {
848
+ return (fs.existsSync(path.join(dir, "app")) ||
849
+ fs.existsSync(path.join(dir, "src", "app")) ||
850
+ fs.existsSync(path.join(dir, "pages")) ||
851
+ fs.existsSync(path.join(dir, "src", "pages"))) && fs.existsSync(path.join(dir, "package.json"));
852
+ }
844
853
  /**
845
854
  * Auto-detect target directory for Next.js app
846
855
  */
847
856
  function detectTargetDirectory(startDir = process.cwd()) {
848
857
  let target = startDir;
849
- // Check if we're in a Next.js app (has app/, src/app/, pages/, or src/pages/ directory)
850
- const hasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
851
- const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
852
- if (!hasAppDir && !hasPagesDir) {
853
- // Try parent directory
854
- const parent = path.resolve(target, "..");
855
- if (fs.existsSync(path.join(parent, "app")) ||
856
- fs.existsSync(path.join(parent, "src", "app")) ||
857
- fs.existsSync(path.join(parent, "pages")) ||
858
- fs.existsSync(path.join(parent, "src", "pages"))) {
859
- target = parent;
860
- }
861
- else {
862
- // Try common monorepo locations: apps/, packages/, or root
863
- const possiblePaths = [
864
- path.join(target, "apps"),
865
- path.join(parent, "apps"),
866
- path.join(target, "packages"),
867
- path.join(parent, "packages"),
868
- ];
869
- let foundApp = false;
870
- for (const possiblePath of possiblePaths) {
871
- if (fs.existsSync(possiblePath)) {
872
- // Look for Next.js apps in this directory
873
- const entries = fs.readdirSync(possiblePath, { withFileTypes: true });
874
- for (const entry of entries) {
875
- if (entry.isDirectory()) {
876
- const appPath = path.join(possiblePath, entry.name);
877
- if (fs.existsSync(path.join(appPath, "app")) ||
878
- fs.existsSync(path.join(appPath, "src", "app")) ||
879
- fs.existsSync(path.join(appPath, "pages")) ||
880
- fs.existsSync(path.join(appPath, "src", "pages"))) {
881
- target = appPath;
882
- foundApp = true;
883
- break;
884
- }
858
+ // Check if we're in a Next.js app
859
+ if (isNextJsApp(target)) {
860
+ return target;
861
+ }
862
+ // Try parent directory
863
+ const parent = path.resolve(target, "..");
864
+ if (isNextJsApp(parent)) {
865
+ return parent;
866
+ }
867
+ // Try common monorepo locations: apps/, packages/, or root
868
+ const possiblePaths = [
869
+ path.join(target, "apps"),
870
+ path.join(parent, "apps"),
871
+ path.join(target, "packages"),
872
+ path.join(parent, "packages"),
873
+ ];
874
+ for (const possiblePath of possiblePaths) {
875
+ if (fs.existsSync(possiblePath)) {
876
+ // Look for Next.js apps in this directory
877
+ try {
878
+ const entries = fs.readdirSync(possiblePath, { withFileTypes: true });
879
+ for (const entry of entries) {
880
+ if (entry.isDirectory()) {
881
+ const appPath = path.join(possiblePath, entry.name);
882
+ if (isNextJsApp(appPath)) {
883
+ return appPath;
885
884
  }
886
885
  }
887
- if (foundApp)
888
- break;
889
886
  }
890
887
  }
888
+ catch {
889
+ // Ignore errors reading directory
890
+ }
891
891
  }
892
892
  }
893
+ // Try searching subdirectories (one level deep) in current directory
894
+ try {
895
+ const entries = fs.readdirSync(target, { withFileTypes: true });
896
+ for (const entry of entries) {
897
+ if (entry.isDirectory()) {
898
+ const subPath = path.join(target, entry.name);
899
+ if (isNextJsApp(subPath)) {
900
+ return subPath;
901
+ }
902
+ }
903
+ }
904
+ }
905
+ catch {
906
+ // Ignore errors reading directory
907
+ }
893
908
  return target;
894
909
  }
895
910
 
@@ -1436,15 +1451,73 @@ async function addPatch(patchName, targetDir) {
1436
1451
  const hasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
1437
1452
  if (!hasAppDir && !hasPagesDir) {
1438
1453
  console.log(chalk$1.yellow("⚠️ Could not auto-detect Next.js app directory."));
1454
+ // Try to find Next.js apps in subdirectories
1455
+ let foundApps = [];
1456
+ try {
1457
+ const entries = fs.readdirSync(target, { withFileTypes: true });
1458
+ for (const entry of entries) {
1459
+ if (entry.isDirectory()) {
1460
+ const subPath = path.join(target, entry.name);
1461
+ const hasSubAppDir = fs.existsSync(path.join(subPath, "app")) || fs.existsSync(path.join(subPath, "src", "app"));
1462
+ const hasSubPagesDir = fs.existsSync(path.join(subPath, "pages")) || fs.existsSync(path.join(subPath, "src", "pages"));
1463
+ const hasPackageJson = fs.existsSync(path.join(subPath, "package.json"));
1464
+ if ((hasSubAppDir || hasSubPagesDir) && hasPackageJson) {
1465
+ foundApps.push(entry.name);
1466
+ }
1467
+ }
1468
+ }
1469
+ }
1470
+ catch {
1471
+ // Ignore errors
1472
+ }
1473
+ let defaultPath = target;
1474
+ if (foundApps.length === 1) {
1475
+ defaultPath = path.join(target, foundApps[0]);
1476
+ console.log(chalk$1.green(`💡 Found Next.js app in subdirectory: ${foundApps[0]}`));
1477
+ }
1478
+ else if (foundApps.length > 1) {
1479
+ console.log(chalk$1.cyan(`💡 Found multiple Next.js apps: ${foundApps.join(", ")}`));
1480
+ }
1439
1481
  const { userTarget } = await inquirer$1.prompt([
1440
1482
  {
1441
1483
  type: "input",
1442
1484
  name: "userTarget",
1443
1485
  message: "Enter the path to your Next.js app folder:",
1444
- default: target,
1486
+ default: defaultPath,
1445
1487
  },
1446
1488
  ]);
1447
- target = path.resolve(userTarget);
1489
+ // Resolve the path - handle both absolute and relative paths
1490
+ if (path.isAbsolute(userTarget)) {
1491
+ target = userTarget;
1492
+ }
1493
+ else {
1494
+ // If relative, resolve from current working directory
1495
+ target = path.resolve(process.cwd(), userTarget);
1496
+ }
1497
+ // Verify the target exists
1498
+ if (!fs.existsSync(target)) {
1499
+ console.log(chalk$1.red(`❌ Error: Directory does not exist: ${target}`));
1500
+ console.log(chalk$1.yellow(`💡 Tip: Make sure you're in the correct directory or provide the full absolute path.`));
1501
+ console.log(chalk$1.yellow(` Current directory: ${process.cwd()}`));
1502
+ if (foundApps.length > 0) {
1503
+ console.log(chalk$1.yellow(` Found Next.js apps in subdirectories: ${foundApps.join(", ")}`));
1504
+ }
1505
+ process.exit(1);
1506
+ }
1507
+ // Verify it's actually a Next.js app
1508
+ const finalHasAppDir = fs.existsSync(path.join(target, "app")) || fs.existsSync(path.join(target, "src", "app"));
1509
+ const finalHasPagesDir = fs.existsSync(path.join(target, "pages")) || fs.existsSync(path.join(target, "src", "pages"));
1510
+ const finalHasPackageJson = fs.existsSync(path.join(target, "package.json"));
1511
+ if (!finalHasAppDir && !finalHasPagesDir) {
1512
+ console.log(chalk$1.red(`❌ Error: ${target} does not appear to be a Next.js app directory.`));
1513
+ console.log(chalk$1.yellow(` Expected to find: app/, src/app/, pages/, or src/pages/ directory`));
1514
+ process.exit(1);
1515
+ }
1516
+ if (!finalHasPackageJson) {
1517
+ console.log(chalk$1.red(`❌ Error: package.json not found in ${target}.`));
1518
+ console.log(chalk$1.yellow(` Make sure you're pointing to the root of your Next.js project.`));
1519
+ process.exit(1);
1520
+ }
1448
1521
  }
1449
1522
  // For auth patches, use new setup flow
1450
1523
  if (patchName === "auth" || patchName === "auth-ui") {
@@ -2059,6 +2132,15 @@ function scanProject(target) {
2059
2132
  };
2060
2133
  if (deps.next || deps["next"]) {
2061
2134
  scan.framework = "nextjs";
2135
+ // Extract Next.js version
2136
+ const nextVersion = deps.next || deps["next"];
2137
+ if (typeof nextVersion === "string") {
2138
+ // Extract version number (handle ranges like "^16.0.0" or "16.0.0")
2139
+ const versionMatch = nextVersion.match(/(\d+)\.(\d+)\.(\d+)/);
2140
+ if (versionMatch) {
2141
+ scan.nextVersion = versionMatch[0];
2142
+ }
2143
+ }
2062
2144
  }
2063
2145
  // Detect package manager from lock files
2064
2146
  if (fs.existsSync(path.join(target, "pnpm-lock.yaml"))) {
@@ -2610,7 +2692,22 @@ function generateMiddleware(target, config, scan) {
2610
2692
  if (config.protectedRoutes.length === 0) {
2611
2693
  return null;
2612
2694
  }
2613
- const middlewarePath = path.join(target, "middleware.ts");
2695
+ // Next.js 16+ uses proxy.ts instead of middleware.ts
2696
+ const isNext16Plus = scan.nextVersion &&
2697
+ (parseInt(scan.nextVersion.split(".")[0]) >= 16);
2698
+ const middlewareFileName = isNext16Plus ? "proxy.ts" : "middleware.ts";
2699
+ const middlewarePath = path.join(target, middlewareFileName);
2700
+ // Also check for the old filename if migrating
2701
+ const oldMiddlewarePath = path.join(target, isNext16Plus ? "middleware.ts" : "proxy.ts");
2702
+ if (fs.existsSync(oldMiddlewarePath) && oldMiddlewarePath !== middlewarePath) {
2703
+ // Remove old middleware file if it exists
2704
+ try {
2705
+ fs.unlinkSync(oldMiddlewarePath);
2706
+ }
2707
+ catch {
2708
+ // Ignore errors
2709
+ }
2710
+ }
2614
2711
  // Check if middleware already exists
2615
2712
  if (fs.existsSync(middlewarePath)) {
2616
2713
  // Try to update existing middleware
@@ -2638,7 +2735,46 @@ function generateMiddleware(target, config, scan) {
2638
2735
  // Add auth pages to matcher so we can redirect authenticated users away
2639
2736
  const authPages = ["/auth/login", "/auth/signup"];
2640
2737
  const allMatcherPatterns = [...matcherPatterns, ...authPages];
2641
- const middlewareContent = `import { NextRequest, NextResponse } from "next/server";
2738
+ // Next.js 16+ uses proxy.ts with default export
2739
+ const middlewareContent = isNext16Plus
2740
+ ? `import { NextRequest, NextResponse } from "next/server";
2741
+ import { getSessionCookie } from "better-auth/cookies";
2742
+
2743
+ export default async function handler(request: NextRequest) {
2744
+ const pathname = request.nextUrl.pathname;
2745
+
2746
+ // Check if session cookie exists
2747
+ const sessionCookie = getSessionCookie(request);
2748
+
2749
+ // Handle auth pages (login/signup)
2750
+ if (pathname === "/auth/login" || pathname === "/auth/signup") {
2751
+ // If already authenticated, redirect away from auth pages
2752
+ if (sessionCookie) {
2753
+ const redirectParam = request.nextUrl.searchParams.get("redirect");
2754
+ const redirectTo = redirectParam || "/stackpatch";
2755
+ return NextResponse.redirect(new URL(redirectTo, request.url));
2756
+ }
2757
+ // Not authenticated - allow access to auth pages
2758
+ return NextResponse.next();
2759
+ }
2760
+
2761
+ // Handle protected routes (only protected routes reach here thanks to matcher)
2762
+ if (!sessionCookie) {
2763
+ // Not authenticated - redirect to login with return URL
2764
+ const loginUrl = new URL("/auth/login", request.url);
2765
+ loginUrl.searchParams.set("redirect", pathname);
2766
+ return NextResponse.redirect(loginUrl);
2767
+ }
2768
+
2769
+ // Authenticated and accessing protected route - allow access
2770
+ return NextResponse.next();
2771
+ }
2772
+
2773
+ export const config = {
2774
+ matcher: ${JSON.stringify(allMatcherPatterns)}, // Protected routes + auth pages
2775
+ };
2776
+ `
2777
+ : `import { NextRequest, NextResponse } from "next/server";
2642
2778
  import { getSessionCookie } from "better-auth/cookies";
2643
2779
 
2644
2780
  export async function middleware(request: NextRequest) {
@@ -3208,7 +3344,10 @@ function showSuccessMessage(target, config, scan) {
3208
3344
  console.log(chalk$1.white(` - ${libDir}/auth-client.ts`));
3209
3345
  console.log(chalk$1.white(` - app/api/auth/[...all]/route.ts`));
3210
3346
  if (config.protectedRoutes.length > 0) {
3211
- console.log(chalk$1.white(` - middleware.ts`));
3347
+ const middlewareFileName = scan.nextVersion && parseInt(scan.nextVersion.split(".")[0]) >= 16
3348
+ ? "proxy.ts"
3349
+ : "middleware.ts";
3350
+ console.log(chalk$1.white(` - ${middlewareFileName}`));
3212
3351
  }
3213
3352
  if (config.protectedRoutes.length > 0) {
3214
3353
  console.log(chalk$1.white(` - ${libDir}/protected-routes.ts`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackpatch",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Composable frontend features for modern React & Next.js apps - Add authentication, UI components, and more with zero configuration",
5
5
  "main": "dist/stackpatch.js",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "test:watch": "vitest",
21
21
  "test:coverage": "vitest run --coverage",
22
22
  "test:cli": "node scripts/test-cli-execution.js",
23
+ "test:nextjs-version": "node scripts/test-nextjs-version.js",
23
24
  "prepublishOnly": "npm run build && node scripts/prepare-publish.js && npm run test:cli",
24
25
  "dev": "bun run bin/stackpatch.ts",
25
26
  "create": "bun run bin/stackpatch.ts"