Skip to content

sf project retrieve/deploy --metadata crashes ENOTDIR when a non-component file is in lwc/ (.forceignore ignored on this path) #3627

Description

@kris-greenheart

Summary

sf project retrieve start --metadata <any> and sf project deploy start --metadata <any> crash with

Error (ENOTDIR): ENOTDIR: not a directory, scandir '.../lwc/<file>'

whenever a non-component file (e.g. README.md, .DS_Store, a docs file) sits as a direct child of the lwc/ directory — even when that file is matched by a .forceignore rule. The .forceignore entry is never consulted on the crashing code path.

Reproduction

  1. Create a valid bundle force-app/main/default/lwc/sampleCmp/ (.js + .html + .js-meta.xml).
  2. Add a plain file force-app/main/default/lwc/README.md.
  3. Add **/README.md (or **/*.md) to .forceignore.
  4. sf project retrieve start --metadata "ApexClass:AnyClass" --target-org <org>

Result: Error (ENOTDIR): ENOTDIR: not a directory, scandir '.../lwc/README.md'. Note the retrieved metadata (ApexClass) is unrelated to LWC — any --metadata target triggers it, because building the retrieve/delete targets resolves the whole project locally. Removing the file, or the .forceignore rule, makes no difference to the ignore behavior on this path.

sf project deploy start --metadata <any> fails the same way. --manifest-based deploys and convert are unaffected (they use the recursive resolver, which does honor .forceignore).

Versions

Reproduced on @salesforce/cli 2.133.4 and 2.147.7 (latest); @salesforce/source-deploy-retrieve 12.35.1.

Root cause

MetadataResolver.getComponentsFromPath() routes to the recursive scan only when the path is a directory; for a file it falls straight through to resolveComponent(fsPath) without the this.forceIgnore.denies(fsPath) guard that the recursive branch applies. A file inside the strict lwc/ type directory resolves to LightningComponentBundleBundleSourceAdapter.getRootMetadataXmlPath() treats the file path as a bundle folder → tree.find()readDirectory(<the file>)readdirSync on a file → ENOTDIR.

Cause stack (--dev-debug):

NodeFSTreeContainer.readDirectory        resolve/treeContainers.ts   (readdirSync)
NodeFSTreeContainer.find                 resolve/treeContainers.ts
BundleSourceAdapter.getRootMetadataXmlPath  resolve/adapters/mixedContentSourceAdapter.ts
BundleSourceAdapter.getComponent         resolve/adapters/baseSourceAdapter.ts
MetadataResolver.resolveComponent        resolve/metadataResolver.ts
MetadataResolver.getComponentsFromPath   resolve/metadataResolver.ts  (single-path branch)
ComponentSetBuilder.build                collections/componentSetBuilder.ts
buildRetrieveAndDeleteTargets            (plugin-deploy-retrieve) commands/project/retrieve/start.ts

Suggested fix

Apply the .forceignore gate on the single-path branch of getComponentsFromPath before dispatching to a source adapter (the recursive branch already does this), and/or have getRootMetadataXmlPath/TreeContainer.find guard with an isDirectory check before readDirectory.

Impact / workaround

Breaks the standard "retrieve before editing" flow for any team that keeps a non-component file (README, editor/AI guidance doc) inside lwc/. Only workaround today is to move the file out of lwc/ for the duration of the command; .forceignore does not help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssue or pull request that identifies or fixes a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions