heygen-com/hyperframes · error
[deploySite] projectDir is not a directory: ${opts.projectDi
Error message
[deploySite] projectDir is not a directory: ${opts.projectDir} What it means
Thrown by deploySite when statSync(opts.projectDir).isDirectory() returns false. Because statSync (not lstat) is called without an existence guard, a non-existent path produces a raw ENOENT from statSync first; this specific message fires only when the path EXISTS but is not a directory (a regular file, a FIFO, etc.). deploySite then tars and uploads the directory, so a file path would corrupt the upload.
Source
Thrown at packages/aws-lambda/src/sdk/deploySite.ts:65
projectS3Uri: string;
/** Tarball size in bytes; useful for "did we actually skip the upload?" assertions. */
bytes: number;
/** ISO timestamp of the most recent upload OR the existing object the short-circuit found. */
uploadedAt: string;
/** `false` if the object already existed and we skipped the PUT. */
uploaded: boolean;
}
/**
* Upload `projectDir` to `s3://bucketName/sites/<siteId>/project.tar.gz`.
*
* Short-circuits when an object with the same key already exists in the
* bucket — `siteId` derives from the project's content hash, so the same
* bytes produce the same key, and re-uploading would be redundant.
*/
export async function deploySite(opts: DeploySiteOptions): Promise<SiteHandle> {
if (!statSync(opts.projectDir).isDirectory()) {
throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);
}
const siteId = opts.siteId ?? hashProjectDir(opts.projectDir);
const key = `sites/${siteId}/project.tar.gz`;
const projectS3Uri = formatS3Uri({ bucket: opts.bucketName, key });
const s3 = opts.s3 ?? new S3Client({ region: opts.region });
// HeadObject short-circuit. Adopters re-rendering the same project on
// a tight inner loop (CI smoke, demo flows) save the tar+gzip+PUT pass
// on every iteration.
const existing = await headObject(s3, opts.bucketName, key);
if (existing) {
return {
siteId,
bucketName: opts.bucketName,
projectS3Uri,
bytes: existing.bytes,
uploadedAt: existing.lastModified,View on GitHub (pinned to c2996c8626)
Solutions
- Point projectDir at the directory CONTAINING index.html, not the file itself.
- Add a pre-check: if (!statSync(p).isDirectory()) throw a clear message before calling deploySite.
- If the path may not exist, guard with existsSync first to give a clearer 'does not exist' message than statSync's ENOENT.
- Resolve symlinks with realpathSync to confirm the target is a directory.
Example fix
// before
await deploySite({ projectDir: './dist/index.html', bucketName });
// after
await deploySite({ projectDir: './dist', bucketName }); Defensive patterns
Strategy: validation
Validate before calling
import { existsSync, statSync } from 'node:fs';
function assertProjectDir(projectDir: string): void {
if (!existsSync(projectDir)) throw new Error(`projectDir does not exist: ${projectDir}`);
if (!statSync(projectDir).isDirectory()) throw new Error(`projectDir is not a directory: ${projectDir}`);
} Type guard
import { statSync } from 'node:fs';
const isValidProjectDir = (p: string): boolean => {
try { return statSync(p).isDirectory(); } catch { return false; }
}; Prevention
- Point projectDir at the directory containing index.html, not the file.
- Pre-check with existsSync to give a clearer 'missing' message than statSync's ENOENT.
- Resolve symlinks with realpathSync before the call.
When it happens
Trigger: Passing projectDir that resolves to a file (e.g. the index.html itself instead of its parent directory), a symlink pointing to a file, or a path that exists but is a socket/device. The ENOENT-from-statSync case (path missing entirely) is a separate unguarded throw.
Common situations: User points --project-dir at ./dist/index.html instead of ./dist; a config that resolves projectDir to a build output file; symlink-to-file; passing a tarball path instead of the unpacked project root.
Related errors
- [s3Transport] upload source missing: ${localPath}
- [s3Transport] tar source must be an existing directory: ${so
- [s3Transport] expected s3:// URI, got: ${JSON.stringify(uri)
- [s3Transport] missing key in s3 URI: ${JSON.stringify(uri)}
- [s3Transport] empty bucket or key in s3 URI: ${JSON.stringif
AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12).
Data as JSON: /api/errors/3bf80f0ac2131ed1.
Report an issue: GitHub.