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
5 changes: 5 additions & 0 deletions .changeset/kind-lark-slim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/node': patch
---

Fixes an infinite loop in `resolveClientDir()` when the server entry point is bundled with esbuild or similar tools. The function now throws a descriptive error instead of hanging indefinitely when the expected server directory segment is not found in the file path.
17 changes: 17 additions & 0 deletions packages/integrations/node/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const STATIC_HEADERS_FILE = '_headers.json';
*
* At build time, we know the relative path between server and client directories.
* At runtime, we need to find the actual location based on where the server entry is running.
*
* ## Error
*
* It throws an error if it can't find the directory while walking the parent directories.
*/
export function resolveClientDir(options: Options) {
// options.client and options.server are file:// URLs set at build time
Expand All @@ -26,7 +30,20 @@ export function resolveClientDir(options: Options) {
// We need to find the actual runtime location, not the build-time paths
const serverFolder = path.basename(options.server);
let serverEntryFolderURL = path.dirname(import.meta.url);
let previous = '';
while (!serverEntryFolderURL.endsWith(serverFolder)) {
// Guard against infinite loop
if (serverEntryFolderURL === previous) {
throw new Error(
`[@astrojs/node] Could not find the server directory "${serverFolder}" ` +
`by walking up from "${import.meta.url}". This can happen when the server ` +
`entry point is bundled into a single file (e.g. with esbuild) so that ` +
`import.meta.url no longer contains the original "${serverFolder}" path segment. ` +
`When bundling the server entry, make sure the output path contains a ` +
`"${serverFolder}" directory segment, or avoid bundling the server entry entirely.`,
);
}
previous = serverEntryFolderURL;
serverEntryFolderURL = path.dirname(serverEntryFolderURL);
}

Expand Down
36 changes: 36 additions & 0 deletions packages/integrations/node/test/units/resolve-client-dir.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { pathToFileURL } from 'node:url';
import { resolveClientDir } from '../../dist/shared.js';

describe('resolveClientDir', () => {
it('throws a descriptive error when the server folder is not found in the path', () => {
// Use pathToFileURL to build platform-valid file:// URLs. On Windows,
// file URLs require a drive letter (e.g. file:///C:/…); bare
// file:///project/… would cause fileURLToPath() to throw before the
// loop guard is reached.
const root = new URL('project/dist/', pathToFileURL('/'));
const client = new URL('client/', root).href;
const server = new URL('server/', root).href;

// When import.meta.url (of shared.js) does not contain a "server" segment,
// the while loop should terminate and throw instead of looping forever.
// This simulates what happens when the entry point is bundled with esbuild
// into a path that lacks the expected "server" directory segment.
assert.throws(
() =>
resolveClientDir({
client,
server,
mode: 'middleware',
host: false,
port: 4321,
staticHeaders: false,
bodySizeLimit: 0,
}),
{
message: /Could not find the server directory "server".*bundled into a single file/,
},
);
});
});
Loading