diff --git a/eslint-suppressions.json b/eslint-suppressions.json index a9bab0726..a76f9b445 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -334,11 +334,8 @@ } }, "test/resolve/treeContainers.test.ts": { - "@typescript-eslint/ban-ts-comment": { - "count": 5 - }, "@typescript-eslint/no-shadow": { - "count": 5 + "count": 1 } }, "test/snapshot/helper/conversions.ts": { diff --git a/src/resolve/adapters/bundleSourceAdapter.ts b/src/resolve/adapters/bundleSourceAdapter.ts index 0728ddcbf..44d72a03f 100644 --- a/src/resolve/adapters/bundleSourceAdapter.ts +++ b/src/resolve/adapters/bundleSourceAdapter.ts @@ -57,9 +57,18 @@ export class BundleSourceAdapter extends MixedContentSourceAdapter { * @protected */ protected populate(trigger: SourcePath, component?: SourceComponent): SourceComponent | undefined { - if (this.tree.isDirectory(trigger) && !this.tree.readDirectory(trigger)?.length) { - // if it's an empty directory, don't include it (e.g., lwc/emptyLWC) - return; + if (this.tree.isDirectory(trigger)) { + if (!this.tree.readDirectory(trigger)?.length) { + // if it's an empty directory, don't include it (e.g., lwc/emptyLWC) + return; + } + } else if (!component) { + const componentRoot = this.trimPathToContent(trigger); + if (!this.tree.isDirectory(componentRoot)) { + // the file sits directly inside the type directory (e.g., lwc/README.md) + // rather than inside a component bundle folder — it is not a valid component + return; + } } return super.populate(trigger, component); } diff --git a/src/resolve/adapters/mixedContentSourceAdapter.ts b/src/resolve/adapters/mixedContentSourceAdapter.ts index 550015da6..6df14a33e 100644 --- a/src/resolve/adapters/mixedContentSourceAdapter.ts +++ b/src/resolve/adapters/mixedContentSourceAdapter.ts @@ -55,6 +55,7 @@ export class MixedContentSourceAdapter extends BaseSourceAdapter { protected getRootMetadataXmlPath(trigger: SourcePath): SourcePath | undefined { if (this.ownFolder) { const componentRoot = this.trimPathToContent(trigger); + if (!this.tree.isDirectory(componentRoot)) return undefined; const rootSuffixes = [this.type.suffix, this.type.legacySuffix].filter( (suffix): suffix is string => typeof suffix === 'string' diff --git a/src/resolve/treeContainers.ts b/src/resolve/treeContainers.ts index e67151364..e0a8e6fba 100644 --- a/src/resolve/treeContainers.ts +++ b/src/resolve/treeContainers.ts @@ -124,7 +124,9 @@ export class NodeFSTreeContainer extends TreeContainer { } public readDirectory(fsPath: SourcePath): string[] { - return readdirSync(this.getUpdatedFsPath(fsPath)); + const resolved = this.getUpdatedFsPath(fsPath); + if (!statSync(resolved).isDirectory()) return []; + return readdirSync(resolved); } public readFile(fsPath: SourcePath): Promise { diff --git a/test/resolve/adapters/bundleSourceAdapter.test.ts b/test/resolve/adapters/bundleSourceAdapter.test.ts index 688c970a4..39638579d 100644 --- a/test/resolve/adapters/bundleSourceAdapter.test.ts +++ b/test/resolve/adapters/bundleSourceAdapter.test.ts @@ -14,23 +14,29 @@ * limitations under the License. */ +import { join } from 'node:path'; import { expect } from 'chai'; import { bundle, lwcBundle } from '../../mock'; import { BundleSourceAdapter } from '../../../src/resolve/adapters'; import { CONTENT_PATH } from '../../mock/type-constants/auraBundleConstant'; import { CONTENT_PATH as LWC_CONTENT_PATH } from '../../mock/type-constants/lwcBundleConstant'; -import { RegistryAccess } from '../../../src'; +import { RegistryAccess, registry, VirtualTreeContainer } from '../../../src'; describe('BundleSourceAdapter with AuraBundle', () => { const registryAccess = new RegistryAccess(); - const adapter = new BundleSourceAdapter(bundle.COMPONENT.type, registryAccess, undefined, bundle.COMPONENT.tree); + const sharedAdapter = new BundleSourceAdapter( + bundle.COMPONENT.type, + registryAccess, + undefined, + bundle.COMPONENT.tree + ); it('Should return expected SourceComponent when given a root metadata xml path', () => { - expect(adapter.getComponent(bundle.XML_PATH)).to.deep.equal(bundle.COMPONENT); + expect(sharedAdapter.getComponent(bundle.XML_PATH)).to.deep.equal(bundle.COMPONENT); }); it('Should return expected SourceComponent when given a bundle directory', () => { - expect(adapter.getComponent(bundle.CONTENT_PATH)).to.deep.equal(bundle.COMPONENT); + expect(sharedAdapter.getComponent(bundle.CONTENT_PATH)).to.deep.equal(bundle.COMPONENT); }); it('Should exclude empty bundle directories', () => { @@ -45,7 +51,7 @@ describe('BundleSourceAdapter with AuraBundle', () => { it('Should return expected SourceComponent when given a source path', () => { const randomSource = bundle.SOURCE_PATHS[1]; - expect(adapter.getComponent(randomSource)).to.deep.equal(bundle.COMPONENT); + expect(sharedAdapter.getComponent(randomSource)).to.deep.equal(bundle.COMPONENT); }); describe('deeply nested LWC', () => { @@ -78,4 +84,41 @@ describe('BundleSourceAdapter with AuraBundle', () => { expect(emptyBundleAdapter.getComponent(LWC_CONTENT_PATH)).to.be.undefined; }); }); + + describe('non-component files in bundle type directory', () => { + const type = registry.types.lightningcomponentbundle; + const typeDir = join('force-app', 'main', 'default', type.directoryName); + const readmePath = join(typeDir, 'README.md'); + const dsStorePath = join(typeDir, '.DS_Store'); + const componentName = 'validCmp'; + const componentDir = join(typeDir, componentName); + + const tree = new VirtualTreeContainer([ + { + dirPath: typeDir, + children: [componentName, 'README.md', '.DS_Store'], + }, + { + dirPath: componentDir, + children: [`${componentName}.js`, `${componentName}.js-meta.xml`], + }, + ]); + + const adapter = new BundleSourceAdapter(type, registryAccess, undefined, tree); + + it('Should return undefined for a non-component file (README.md) in lwc/', () => { + expect(adapter.getComponent(readmePath)).to.be.undefined; + }); + + it('Should return undefined for a dot file (.DS_Store) in lwc/', () => { + expect(adapter.getComponent(dsStorePath)).to.be.undefined; + }); + + it('Should still resolve a valid component directory', () => { + const result = adapter.getComponent(componentDir); + expect(result).to.not.be.undefined; + expect(result?.type).to.deep.equal(type); + expect(result?.name).to.equal(componentName); + }); + }); }); diff --git a/test/resolve/metadataResolver.test.ts b/test/resolve/metadataResolver.test.ts index ade4da185..d5b63297c 100644 --- a/test/resolve/metadataResolver.test.ts +++ b/test/resolve/metadataResolver.test.ts @@ -561,6 +561,19 @@ describe('MetadataResolver', () => { expect(mdResolver.getComponentsFromPath(nonMetadataDirPath, filter)).to.deep.equal([]); }); + it('Should return empty array for a non-component file directly inside a bundle type directory (lwc/)', () => { + const lwcDir = join('force-app', 'main', 'default', 'lwc'); + const readmePath = join(lwcDir, 'README.md'); + const cmpDir = join(lwcDir, 'myCmp'); + const treeContainer = new VirtualTreeContainer([ + { dirPath: join('force-app', 'main', 'default'), children: ['lwc'] }, + { dirPath: lwcDir, children: ['myCmp', 'README.md'] }, + { dirPath: cmpDir, children: ['myCmp.js', 'myCmp.js-meta.xml'] }, + ]); + const mdResolver = new MetadataResolver(undefined, treeContainer, false); + expect(mdResolver.getComponentsFromPath(readmePath)).to.deep.equal([]); + }); + it('Should resolve RestrictionRules metadata in mdapi format', () => { const unpackagedPath = 'unpackaged'; const packageXmlPath = join(unpackagedPath, 'package.xml'); diff --git a/test/resolve/treeContainers.test.ts b/test/resolve/treeContainers.test.ts index b8863c953..cfb99c767 100644 --- a/test/resolve/treeContainers.test.ts +++ b/test/resolve/treeContainers.test.ts @@ -88,58 +88,66 @@ describe('Tree Containers', () => { describe('NodeFSTreeContainer', () => { const env = createSandbox(); - const tree = new NodeFSTreeContainer(); - const path = join('path', 'to', 'test'); + const sharedTree = new NodeFSTreeContainer(); + const sharedPath = join('path', 'to', 'test'); afterEach(() => env.restore()); it('should use expected Node API for exists', () => { const existsStub = env.stub(fs, 'existsSync'); - existsStub.withArgs(path).returns(true); - expect(tree.exists(path)).to.be.true; + existsStub.withArgs(sharedPath).returns(true); + expect(sharedTree.exists(sharedPath)).to.be.true; expect(existsStub.calledOnce).to.be.true; }); it('should use expected Node API for isDirectory', () => { const statStub = env.stub(fs, 'statSync'); - // @ts-ignore lstat returns more than isDirectory function - statStub.withArgs(path).returns({ isDirectory: () => true }); - expect(tree.isDirectory(path)).to.be.true; + // @ts-expect-error lstat returns more than isDirectory function + statStub.withArgs(sharedPath).returns({ isDirectory: () => true }); + expect(sharedTree.isDirectory(sharedPath)).to.be.true; expect(statStub.calledOnce).to.be.true; }); it('should use expected Node API for readDirectory', () => { + const statStub = env.stub(fs, 'statSync'); + // @ts-expect-error lstat returns more than isDirectory function + statStub.withArgs(sharedPath).returns({ isDirectory: () => true }); const readdirStub = env.stub(fs, 'readdirSync'); - // @ts-ignore wants Dirents but string[] works as well - readdirStub.withArgs(path).returns(readDirResults); - expect(tree.readDirectory(path)).to.deep.equal(readDirResults); + // @ts-expect-error wants Dirents but string[] works as well + readdirStub.withArgs(sharedPath).returns(readDirResults); + expect(sharedTree.readDirectory(sharedPath)).to.deep.equal(readDirResults); expect(readdirStub.calledOnce).to.be.true; }); + it('should return empty array for readDirectory on a file path', () => { + const statStub = env.stub(fs, 'statSync'); + // @ts-expect-error lstat returns more than isDirectory function + statStub.withArgs(sharedPath).returns({ isDirectory: () => false }); + expect(sharedTree.readDirectory(sharedPath)).to.deep.equal([]); + }); + it('should use expected Node API for readFile', async () => { const readFileStub = env.stub(fs, 'readFileSync'); - // @ts-ignore wants Dirents but string[] works as well - readFileStub.withArgs(path).resolves(Buffer.from('test')); - const data = await tree.readFile(path); + readFileStub.withArgs(sharedPath).resolves(Buffer.from('test')); + const data = await sharedTree.readFile(sharedPath); expect(data.toString()).to.deep.equal('test'); expect(readFileStub.calledOnce).to.be.true; }); it('should use expected Node API for readFileSync', () => { const readFileStub = env.stub(fs, 'readFileSync'); - // @ts-ignore wants Dirents but string[] works as well - readFileStub.withArgs(path).returns(Buffer.from('test')); - const data = tree.readFileSync(path); + readFileStub.withArgs(sharedPath).returns(Buffer.from('test')); + const data = sharedTree.readFileSync(sharedPath); expect(data.toString()).to.deep.equal('test'); expect(readFileStub.calledOnce).to.be.true; }); it('should use expected Node API for stream', () => { const readable = new Readable(); - // @ts-ignore wants ReadStream but Readable works for testing + // @ts-expect-error wants ReadStream but Readable works for testing env.stub(fs, 'createReadStream').returns(readable); env.stub(fs, 'existsSync').returns(true); - expect(tree.stream(path)).to.deep.equal(readable); + expect(sharedTree.stream(sharedPath)).to.deep.equal(readable); }); describe('with projectPath/cwd', () => {