Skip to content

Commit 2d301f9

Browse files
authored
fix: @W-17964111 improve component resolution for single component preview (#358)
* fix: improve component resolution * fix: improve error handling
1 parent eb409d7 commit 2d301f9

File tree

7 files changed

+144
-39
lines changed

7 files changed

+144
-39
lines changed

messages/lightning.dev.component.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ Unable to find components
2828

2929
Unable to determine component name
3030

31+
# error.component-metadata
32+
33+
Failed to parse component metadata at: %s
34+
35+
# error.component-not-found
36+
37+
Unable to find component with name: %s
38+
3139
# examples
3240

3341
- Select a component and launch the component preview:

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
"axios": "^1.7.9",
1919
"glob": "^10.4.5",
2020
"lwc": "~8.12.5",
21-
"node-fetch": "^3.3.2"
21+
"node-fetch": "^3.3.2",
22+
"xml2js": "^0.6.2"
2223
},
2324
"devDependencies": {
2425
"@oclif/plugin-command-snapshot": "^5.2.35",
2526
"@salesforce/cli-plugins-testkit": "^5.3.39",
2627
"@salesforce/dev-scripts": "^10.2.11",
2728
"@salesforce/plugin-command-reference": "^3.1.44",
2829
"@types/node-fetch": "^2.6.11",
30+
"@types/xml2js": "^0.4.14",
2931
"eslint-plugin-sf-plugin": "^1.20.15",
3032
"esmock": "^2.6.9",
3133
"oclif": "^4.17.27",

src/commands/lightning/dev/component.ts

Lines changed: 43 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,16 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import fs from 'node:fs';
98
import path from 'node:path';
109
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
11-
import { Messages } from '@salesforce/core';
10+
import { Messages, SfProject } from '@salesforce/core';
1211
import { cmpDev } from '@lwrjs/api';
12+
import { ComponentUtils } from '../../../shared/componentUtils.js';
1313
import { PromptUtils } from '../../../shared/promptUtils.js';
1414

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

18-
// TODO support other module directories
19-
const MODULES_DIR = path.resolve(path.join('force-app', 'main', 'default', 'lwc'));
20-
21-
function getDirectories(filePath: string): string[] {
22-
try {
23-
const items = fs.readdirSync(filePath);
24-
25-
const directories = items.filter((item) => fs.statSync(path.join(filePath, item)).isDirectory());
26-
27-
return directories;
28-
} catch (error) {
29-
return [];
30-
}
31-
}
32-
3318
export default class LightningDevComponent extends SfCommand<void> {
3419
public static readonly summary = messages.getMessage('summary');
3520
public static readonly description = messages.getMessage('description');
@@ -48,35 +33,56 @@ export default class LightningDevComponent extends SfCommand<void> {
4833

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

52-
let name = flags.name;
53-
if (!name) {
54-
const dirs = getDirectories(path.resolve(MODULES_DIR));
55-
if (!dirs) {
56-
throw new Error(messages.getMessage('error.directory'));
57-
}
58-
59-
const components = dirs.map((dir) => {
60-
const xmlPath = path.resolve(path.join(MODULES_DIR, dir, `${dir}.js-meta.xml`));
61-
const xmlContent = fs.readFileSync(xmlPath, 'utf-8');
62-
const label = xmlContent.match(/<masterLabel>(.*?)<\/masterLabel>/);
63-
const description = xmlContent.match(/<description>(.*?)<\/description>/);
38+
const namespacePaths = await ComponentUtils.getNamespacePaths(project);
39+
const componentPaths = await ComponentUtils.getAllComponentPaths(namespacePaths);
40+
if (!componentPaths) {
41+
throw new Error(messages.getMessage('error.directory'));
42+
}
6443

65-
return {
66-
name: dir,
67-
label: label ? label[1] : '',
68-
description: description ? description[1] : '',
69-
};
70-
});
44+
const components = (
45+
await Promise.all(
46+
componentPaths.map(async (componentPath) => {
47+
let xml;
48+
49+
try {
50+
xml = await ComponentUtils.getComponentMetadata(componentPath);
51+
} catch (err) {
52+
this.warn(messages.getMessage('error.component-metadata', [componentPath]));
53+
}
54+
55+
// components must have meta xml to be previewed
56+
if (!xml) {
57+
return undefined;
58+
}
59+
60+
const componentName = path.basename(componentPath);
61+
const label = ComponentUtils.componentNameToTitleCase(componentName);
62+
63+
return {
64+
name: componentName,
65+
label: xml.LightningComponentBundle.masterLabel ?? label,
66+
description: xml.LightningComponentBundle.description ?? '',
67+
};
68+
})
69+
)
70+
).filter((component) => !!component);
7171

72+
let name = flags.name;
73+
if (name) {
74+
// validate that the component exists before launching the server
75+
if (!components.find((component) => name === component.name)) {
76+
throw new Error(messages.getMessage('error.component-not-found', [name]));
77+
}
78+
} else {
79+
// prompt the user for a name if one was not provided
7280
name = await PromptUtils.promptUserToSelectComponent(components);
7381
if (!name) {
7482
throw new Error(messages.getMessage('error.component'));
7583
}
7684
}
7785

78-
this.log('Starting application on port 3000...');
79-
8086
const port = parseInt(process.env.PORT ?? '3000', 10);
8187

8288
await cmpDev({

src/shared/componentUtils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import path from 'node:path';
8+
import fs from 'node:fs';
9+
import { glob } from 'glob';
10+
import { parseStringPromise } from 'xml2js';
11+
import { SfProject } from '@salesforce/core';
12+
13+
export type LwcMetadata = {
14+
LightningComponentBundle: {
15+
description?: string;
16+
masterLabel?: string;
17+
};
18+
};
19+
20+
export class ComponentUtils {
21+
public static componentNameToTitleCase(componentName: string): string {
22+
if (!componentName) {
23+
return '';
24+
}
25+
26+
return componentName.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
27+
}
28+
29+
public static async getNamespacePaths(project: SfProject): Promise<string[]> {
30+
const packageDirs = project.getPackageDirectories();
31+
32+
return (await Promise.all(packageDirs.map((dir) => glob(`${dir.fullPath}/**/lwc`, { absolute: true })))).flat();
33+
}
34+
35+
public static async getAllComponentPaths(namespacePaths: string[]): Promise<string[]> {
36+
return (
37+
await Promise.all(namespacePaths.map((namespacePath) => ComponentUtils.getComponentPaths(namespacePath)))
38+
).flat();
39+
}
40+
41+
public static async getComponentPaths(namespacePath: string): Promise<string[]> {
42+
const children = await fs.promises.readdir(namespacePath, { withFileTypes: true });
43+
44+
return children.filter((child) => child.isDirectory()).map((child) => path.join(child.parentPath, child.name));
45+
}
46+
47+
public static async getComponentMetadata(dirname: string): Promise<LwcMetadata | undefined> {
48+
const componentName = path.basename(dirname);
49+
const metaXmlPath = path.join(dirname, `${componentName}.js-meta.xml`);
50+
if (!fs.existsSync(metaXmlPath)) {
51+
return undefined;
52+
}
53+
54+
const xmlContent = await fs.promises.readFile(metaXmlPath, 'utf8');
55+
const parsedData = (await parseStringPromise(xmlContent)) as LwcMetadata;
56+
if (!this.isLwcMetadata(parsedData)) {
57+
return undefined;
58+
}
59+
60+
return parsedData;
61+
}
62+
63+
private static isLwcMetadata(obj: unknown): obj is LwcMetadata {
64+
return (obj && typeof obj === 'object' && 'LightningComponentBundle' in obj) === true;
65+
}
66+
}

src/shared/promptUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export class PromptUtils {
9292

9393
public static async promptUserToSelectComponent(components: Array<Record<string, string>>): Promise<string> {
9494
const choices = components.map((component) => ({
95-
name: component.label.length > 0 ? component.label : component.name,
95+
name: component.label ?? component.name,
9696
value: component.name,
9797
description: component.description,
9898
}));

test/shared/componentUtils.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright (c) 2024, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { expect } from 'chai';
9+
import { ComponentUtils } from '../../src/shared/componentUtils.js';
10+
11+
describe('componentUtils', () => {
12+
it('converts camel case component name to title case', () => {
13+
expect(ComponentUtils.componentNameToTitleCase('myButton')).to.equal('My Button');
14+
expect(ComponentUtils.componentNameToTitleCase('myButtonGroup')).to.equal('My Button Group');
15+
});
16+
});

yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4162,6 +4162,13 @@
41624162
resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
41634163
integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==
41644164

4165+
"@types/xml2js@^0.4.14":
4166+
version "0.4.14"
4167+
resolved "https://registry.yarnpkg.com/@types/xml2js/-/xml2js-0.4.14.tgz#5d462a2a7330345e2309c6b549a183a376de8f9a"
4168+
integrity sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==
4169+
dependencies:
4170+
"@types/node" "*"
4171+
41654172
"@typescript-eslint/eslint-plugin@^6.21.0":
41664173
version "6.21.0"
41674174
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"

0 commit comments

Comments
 (0)