Skip to content

excludeNodeDevDependencies includes all dev dependencies if serviceDir has symlinks #13447

@osmoossi

Description

@osmoossi

Issue description

Environment

  • Serverless Framework: 4.33.0
  • Node.js: 22
  • Build environment: AWS CodeBuild (source: CodeStar Connections / git-http)

Description

When deploying from AWS CodeBuild, all dev dependencies are bundled into the Lambda zip instead of being excluded. The root cause is that CodeBuild checks out source code into a directory path that contains symlink components, causing a path mismatch between what Serverless uses as serviceDir and what npm ls --parseable outputs.

The mismatch happens in two steps:

  1. replace() silently fails. The excludeNodeDevDependencies logic in zip-service.js uses String.replace() to strip the serviceDir prefix from npm ls --parseable output. However, serviceDir holds the symlink path while npm ls --parseable outputs real (resolved) paths, because child_process.exec resolves symlinks via chdir() + getcwd() on Linux. The prefix doesn't match, so replace() returns the item unchanged — still a full absolute path like /codebuild/output/src1234/src/.../node_modules/loupe.

  2. path.join doubles the path. When path.join(serviceDir, item, 'package.json') receives an absolute path as the second argument, it doesn't treat it as a root — it strips the leading / and appends it to serviceDir, producing the doubled path seen in the error.

Observed error (from CodeBuild logs)

Unable to read the package.json file at
"/codebuild/output/src1234/src/.../component-name/codebuild/output/src1234/src/.../component-name/node_modules/loupe/package.json"
while processing dependencies for exclusion. Skipping this dependency.
Error: ENOENT: no such file or directory, open '...same doubled path.../package.json'

The path segment /codebuild/output/src1234/src/... appears twice — this is the doubled path. Because the package.json read fails with ENOENT, the dependency is skipped (not excluded), causing dev dependencies like loupe to be bundled.

Possible solution

Replace String.replace() with path.relative() when stripping the service directory prefix:

// Before (buggy)
const stripped = item.replace(path.join(serviceDir, path.sep), '')

// After (correct)
const stripped = path.relative(serviceDir, item)

path.relative() resolves through symlinks on both sides and always produces a correct relative path regardless of whether serviceDir or the npm output uses symlink vs real paths.

Reproduction

See attached repro.js, it creates original_folder with a node_modules tree, symlinks it as symlink_folder, and demonstrates that the buggy replace() fails while path.relative() succeeds.

Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions