Skip to content

[v6] resolveClientDir() causes infinite loop when bundled with esbuild #15709

@ayame113

Description

@ayame113

Astro Info

Astro                    v6.0.0-beta.17
Node                     v24.11.1
System                   macOS (arm64)
Package Manager          npm
Output                   static
Adapter                  @astrojs/node (>=10.0.0-beta.4)
Integrations             @astrojs/react

If this issue only occurs in one browser, which browser is a problem?

AWS Lambda (bundle and deploy with esbuild)

Describe the Bug

Since #15473 was merged, resolveClientDir() (moved to shared.ts) is now called unconditionally inside createAppHandler() in serve-app.ts. This function contains a while loop that walks up parent directories until it finds a folder named "server". When the server entry point is bundled into a single file with esbuild, the runtime path never contains a "server" segment, causing the server to hang indefinitely during initialization.

I encountered this when deploying to AWS Lambda.


Steps to Reproduce

  1. Build an Astro project using @astrojs/node in middleware mode:
npm run build
  1. Create example.js with the following content:
import serverlessExpress from "@codegenie/serverless-express";
import express from "express";
import { handler as ssrHandler } from "./src/frontend/dist/server/entry.mjs";

export const app = express();

const base = "/";
app.use(base, express.static(new URL("./dist/client/", import.meta.url).pathname));
app.use(ssrHandler);

export const handler = serverlessExpress({ app });

console.log("start server");
  1. Bundle with esbuild and run:
npx esbuild example.js \
  --bundle \
  --outfile=out.js \
  --minify \
  --platform=node \
  --format=esm \
  --banner:js="import { createRequire } from 'node:module';const require = createRequire(import.meta.url);"

node out.js
  1. The process hangs and "start server" is never printed.

Possible cause

resolveClientDir() in shared.ts:

export function resolveClientDir(options: Options) {
  const serverFolder = path.basename(options.server); // → "server"
  let serverEntryFolderURL = path.dirname(import.meta.url);
  while (!serverEntryFolderURL.endsWith(serverFolder)) { // ← infinite loop
    serverEntryFolderURL = path.dirname(serverEntryFolderURL);
  }
  // ...
}

When bundled with esbuild, import.meta.url resolves to the bundled file path (e.g. file:///path/to/out.js), which does not contain a "server" segment, so path.dirname() will continue to return the same value once it reaches the root of the filesystem, possibly looping forever.

What's the expected result?

The server should not hang when the entry point is bundled with esbuild.

Link to Minimal Reproducible Example

https://stackblitz.com/edit/github-5vg9qzdg?file=astro.config.mjs

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

Labels

- P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: nodeRelated to Node adapter (scope)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions