Skip to content

Commit

Permalink
fix: improve component resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
bpbuch committed Mar 5, 2025
1 parent eb409d7 commit bfd61b4
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 32 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
"axios": "^1.7.9",
"glob": "^10.4.5",
"lwc": "~8.12.5",
"node-fetch": "^3.3.2"
"node-fetch": "^3.3.2",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.35",
"@salesforce/cli-plugins-testkit": "^5.3.39",
"@salesforce/dev-scripts": "^10.2.11",
"@salesforce/plugin-command-reference": "^3.1.44",
"@types/node-fetch": "^2.6.11",
"@types/xml2js": "^0.4.14",
"eslint-plugin-sf-plugin": "^1.20.15",
"esmock": "^2.6.9",
"oclif": "^4.17.27",
Expand Down
53 changes: 23 additions & 30 deletions src/commands/lightning/dev/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,16 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import fs from 'node:fs';
import path from 'node:path';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Messages, SfProject } from '@salesforce/core';
import { cmpDev } from '@lwrjs/api';
import { ComponentUtils } from '../../../shared/componentUtils.js';
import { PromptUtils } from '../../../shared/promptUtils.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component');

// TODO support other module directories
const MODULES_DIR = path.resolve(path.join('force-app', 'main', 'default', 'lwc'));

function getDirectories(filePath: string): string[] {
try {
const items = fs.readdirSync(filePath);

const directories = items.filter((item) => fs.statSync(path.join(filePath, item)).isDirectory());

return directories;
} catch (error) {
return [];
}
}

export default class LightningDevComponent extends SfCommand<void> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
Expand All @@ -47,27 +32,35 @@ export default class LightningDevComponent extends SfCommand<void> {
};

public async run(): Promise<void> {
const project = await SfProject.resolve();
const { flags } = await this.parse(LightningDevComponent);

let name = flags.name;
if (!name) {
const dirs = getDirectories(path.resolve(MODULES_DIR));
const dirs = await ComponentUtils.getComponentPaths(project);
if (!dirs) {
throw new Error(messages.getMessage('error.directory'));
}

const components = dirs.map((dir) => {
const xmlPath = path.resolve(path.join(MODULES_DIR, dir, `${dir}.js-meta.xml`));
const xmlContent = fs.readFileSync(xmlPath, 'utf-8');
const label = xmlContent.match(/<masterLabel>(.*?)<\/masterLabel>/);
const description = xmlContent.match(/<description>(.*?)<\/description>/);

return {
name: dir,
label: label ? label[1] : '',
description: description ? description[1] : '',
};
});
const components = (
await Promise.all(
dirs.map(async (dir) => {
const xml = await ComponentUtils.getComponentMetadata(dir);
if (!xml) {
return undefined;
}

const componentName = path.basename(dir);
const label = ComponentUtils.componentNameToTitleCase(componentName);

return {
name: componentName,
label: xml.LightningComponentBundle.masterLabel ?? label,
description: xml.LightningComponentBundle.description ?? '',
};
})
)
).filter((component) => !!component);

name = await PromptUtils.promptUserToSelectComponent(components);
if (!name) {
Expand Down
67 changes: 67 additions & 0 deletions src/shared/componentUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import path from 'node:path';
import fs from 'node:fs';
import { SfProject } from '@salesforce/core';
import { glob } from 'glob';
import { parseStringPromise } from 'xml2js';

export type LwcMetadata = {
LightningComponentBundle: {
description?: string;
masterLabel?: string;
};
};

export class ComponentUtils {
public static componentNameToTitleCase(componentName: string): string {
if (!componentName) {
return '';
}

return componentName.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
}

public static async getComponentPaths(project: SfProject): Promise<string[]> {
const packageDirs = project.getPackageDirectories();
const namespacePaths = (
await Promise.all(packageDirs.map((dir) => glob(`${dir.fullPath}/**/lwc`, { absolute: true })))
).flat();

return (
await Promise.all(
namespacePaths.map(async (namespacePath): Promise<string[]> => {
const children = await fs.promises.readdir(namespacePath, { withFileTypes: true });

return children
.filter((child) => child.isDirectory())
.map((child) => path.join(child.parentPath, child.name));
})
)
).flat();
}

public static async getComponentMetadata(dirname: string): Promise<LwcMetadata | undefined> {
const componentName = path.basename(dirname);
const metaXmlPath: string = path.join(dirname, `${componentName}.js-meta.xml`);
if (!fs.existsSync(metaXmlPath)) {
return undefined;
}

const xmlContent: string = fs.readFileSync(metaXmlPath, 'utf8');
const parsedData = (await parseStringPromise(xmlContent)) as LwcMetadata;
if (!this.isLwcMetadata(parsedData)) {
return undefined;
}

return parsedData;
}

private static isLwcMetadata(obj: unknown): obj is LwcMetadata {
return (obj && typeof obj === 'object' && 'LightningComponentBundle' in obj) === true;
}
}
2 changes: 1 addition & 1 deletion src/shared/promptUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class PromptUtils {

public static async promptUserToSelectComponent(components: Array<Record<string, string>>): Promise<string> {
const choices = components.map((component) => ({
name: component.label.length > 0 ? component.label : component.name,
name: component.label ?? component.name,
value: component.name,
description: component.description,
}));
Expand Down
16 changes: 16 additions & 0 deletions test/shared/componentUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { expect } from 'chai';
import { ComponentUtils } from '../../src/shared/componentUtils.js';

describe('componentUtils', () => {
it('converts camel case component name to title case', () => {
expect(ComponentUtils.componentNameToTitleCase('myButton')).to.equal('My Button');
expect(ComponentUtils.componentNameToTitleCase('myButtonGroup')).to.equal('My Button Group');
});
});
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4162,6 +4162,13 @@
resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==

"@types/xml2js@^0.4.14":
version "0.4.14"
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a"
integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==
dependencies:
"@types/node" "*"

"@typescript-eslint/eslint-plugin@^6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
Expand Down

0 comments on commit bfd61b4

Please sign in to comment.