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
- Create a valid bundle
force-app/main/default/lwc/sampleCmp/ (.js + .html + .js-meta.xml).
- Add a plain file
force-app/main/default/lwc/README.md.
- Add
**/README.md (or **/*.md) to .forceignore.
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 LightningComponentBundle → BundleSourceAdapter.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.
Summary
sf project retrieve start --metadata <any>andsf project deploy start --metadata <any>crash withwhenever a non-component file (e.g.
README.md,.DS_Store, a docs file) sits as a direct child of thelwc/directory — even when that file is matched by a.forceignorerule. The.forceignoreentry is never consulted on the crashing code path.Reproduction
force-app/main/default/lwc/sampleCmp/(.js+.html+.js-meta.xml).force-app/main/default/lwc/README.md.**/README.md(or**/*.md) to.forceignore.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--metadatatarget triggers it, because building the retrieve/delete targets resolves the whole project locally. Removing the file, or the.forceignorerule, makes no difference to the ignore behavior on this path.sf project deploy start --metadata <any>fails the same way.--manifest-based deploys andconvertare unaffected (they use the recursive resolver, which does honor.forceignore).Versions
Reproduced on
@salesforce/cli2.133.4 and 2.147.7 (latest);@salesforce/source-deploy-retrieve12.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 toresolveComponent(fsPath)without thethis.forceIgnore.denies(fsPath)guard that the recursive branch applies. A file inside the strictlwc/type directory resolves toLightningComponentBundle→BundleSourceAdapter.getRootMetadataXmlPath()treats the file path as a bundle folder →tree.find()→readDirectory(<the file>)→readdirSyncon a file →ENOTDIR.Cause stack (
--dev-debug):Suggested fix
Apply the
.forceignoregate on the single-path branch ofgetComponentsFromPathbefore dispatching to a source adapter (the recursive branch already does this), and/or havegetRootMetadataXmlPath/TreeContainer.findguard with anisDirectorycheck beforereadDirectory.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 oflwc/for the duration of the command;.forceignoredoes not help.