{"record":{"id":"3bf80f0ac2131ed1","repo":"heygen-com/hyperframes","slug":"deploysite-projectdir-is-not-a-directory-opts","errorCode":null,"errorMessage":"[deploySite] projectDir is not a directory: ${opts.projectDir}","messagePattern":"\\[deploySite\\] projectDir is not a directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/aws-lambda/src/sdk/deploySite.ts","lineNumber":65,"sourceCode":"  projectS3Uri: string;\n  /** Tarball size in bytes; useful for \"did we actually skip the upload?\" assertions. */\n  bytes: number;\n  /** ISO timestamp of the most recent upload OR the existing object the short-circuit found. */\n  uploadedAt: string;\n  /** `false` if the object already existed and we skipped the PUT. */\n  uploaded: boolean;\n}\n\n/**\n * Upload `projectDir` to `s3://bucketName/sites/<siteId>/project.tar.gz`.\n *\n * Short-circuits when an object with the same key already exists in the\n * bucket — `siteId` derives from the project's content hash, so the same\n * bytes produce the same key, and re-uploading would be redundant.\n */\nexport async function deploySite(opts: DeploySiteOptions): Promise<SiteHandle> {\n  if (!statSync(opts.projectDir).isDirectory()) {\n    throw new Error(`[deploySite] projectDir is not a directory: ${opts.projectDir}`);\n  }\n\n  const siteId = opts.siteId ?? hashProjectDir(opts.projectDir);\n  const key = `sites/${siteId}/project.tar.gz`;\n  const projectS3Uri = formatS3Uri({ bucket: opts.bucketName, key });\n  const s3 = opts.s3 ?? new S3Client({ region: opts.region });\n\n  // HeadObject short-circuit. Adopters re-rendering the same project on\n  // a tight inner loop (CI smoke, demo flows) save the tar+gzip+PUT pass\n  // on every iteration.\n  const existing = await headObject(s3, opts.bucketName, key);\n  if (existing) {\n    return {\n      siteId,\n      bucketName: opts.bucketName,\n      projectS3Uri,\n      bytes: existing.bytes,\n      uploadedAt: existing.lastModified,","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/aws-lambda/src/sdk/deploySite.ts#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait deploySite({ projectDir: './dist/index.html', bucketName });\n\n// after\nawait deploySite({ projectDir: './dist', bucketName });","handlingStrategy":"validation","validationCode":"import { existsSync, statSync } from 'node:fs';\nfunction assertProjectDir(projectDir: string): void {\n  if (!existsSync(projectDir)) throw new Error(`projectDir does not exist: ${projectDir}`);\n  if (!statSync(projectDir).isDirectory()) throw new Error(`projectDir is not a directory: ${projectDir}`);\n}","typeGuard":"import { statSync } from 'node:fs';\nconst isValidProjectDir = (p: string): boolean => {\n  try { return statSync(p).isDirectory(); } catch { return false; }\n};","tryCatchPattern":null,"preventionTips":["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."],"tags":["deploy","filesystem","validation","s3"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}