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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions