Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
15 changes: 12 additions & 3 deletions src/resolve/adapters/bundleSourceAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/resolve/adapters/mixedContentSourceAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 3 additions & 1 deletion src/resolve/treeContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> {
Expand Down
53 changes: 48 additions & 5 deletions test/resolve/adapters/bundleSourceAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
13 changes: 13 additions & 0 deletions test/resolve/metadataResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
44 changes: 26 additions & 18 deletions test/resolve/treeContainers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading