Skip to content

Commit 88f36c0

Browse files
authored
feat: add api version validation (#218)
* feat: add api version validation * chore: update comment * chore: use ESM instead of CJS * chore: address feedback
1 parent 515a5e3 commit 88f36c0

File tree

8 files changed

+146
-5
lines changed

8 files changed

+146
-5
lines changed

.husky/check-versions.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import fs from 'node:fs';
2+
3+
// Read package.json
4+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
5+
6+
// Extract versions
7+
const devServerDependencyVersion = packageJson.dependencies['@lwc/lwc-dev-server'];
8+
const devServerTargetVersion = packageJson.apiVersionMetadata?.target?.matchingDevServerVersion;
9+
10+
if (!devServerDependencyVersion || !devServerTargetVersion) {
11+
console.error('Error: missing @lwc/lwc-dev-server or matchingDevServerVersion');
12+
process.exit(1); // Fail the check
13+
}
14+
15+
// Compare versions
16+
if (devServerDependencyVersion === devServerTargetVersion) {
17+
process.exit(0); // Pass the check
18+
} else {
19+
console.error(
20+
`Error: @lwc/lwc-dev-server versions do not match between 'dependencies' and 'apiVersionMetadata' in package.json. Expected ${devServerDependencyVersion} in apiVersionMetadata > target > matchingDevServerVersion. Got ${devServerTargetVersion} instead. When updating the @lwc/lwc-dev-server dependency, you must ensure that it is compatible with the supported API version in this branch, then update apiVersionMetadata > target > matchingDevServerVersion to match, in order to "sign off" on this dependency change.`
21+
);
22+
process.exit(1); // Fail the check
23+
}

.husky/pre-commit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4+
# Run the custom version check script
5+
node .husky/check-versions.js
6+
47
yarn lint && yarn pretty-quick --staged

messages/shared.utils.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@ You must provide valid SSL certificate data
2929
# error.localdev.not.enabled
3030

3131
Local Dev is not enabled for your org. See https://developer.salesforce.com/docs/platform/lwc/guide/get-started-test-components.html for more information on enabling and using Local Dev.
32+
33+
# error.org.api-mismatch.message
34+
35+
Your org is on API version %s, but this version of the CLI plugin supports API version %s.
36+
37+
# error.org.api-mismatch.remediation
38+
39+
To use the plugin with this org, you can reinstall or update the plugin using the "%s" tag. For example: "sf plugins install %s".

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,23 @@
215215
"output": []
216216
}
217217
},
218+
"apiVersionMetadata": {
219+
"comment": "Refer to ApiVersionMetadata in orgUtils.ts for details",
220+
"target": {
221+
"versionNumber": "62.0",
222+
"matchingDevServerVersion": "^9.5.1"
223+
},
224+
"versionToTagMappings": [
225+
{
226+
"versionNumber": "62.0",
227+
"tagName": "latest"
228+
},
229+
{
230+
"versionNumber": "63.0",
231+
"tagName": "next"
232+
}
233+
]
234+
},
218235
"exports": "./lib/index.js",
219236
"type": "module",
220237
"volta": {

src/commands/lightning/dev/app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ export default class LightningDevApp extends SfCommand<void> {
6868

6969
const targetOrg = flags['target-org'];
7070
const appName = flags['name'];
71-
const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform());
7271
const deviceId = flags['device-id'];
7372

7473
let sfdxProjectRootPath = '';
@@ -78,7 +77,6 @@ export default class LightningDevApp extends SfCommand<void> {
7877
return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? ''])));
7978
}
8079

81-
logger.debug('Configuring local web server identity');
8280
const connection = targetOrg.getConnection(undefined);
8381
const username = connection.getUsername();
8482
if (!username) {
@@ -90,6 +88,11 @@ export default class LightningDevApp extends SfCommand<void> {
9088
return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled')));
9189
}
9290

91+
OrgUtils.ensureMatchingAPIVersion(connection);
92+
93+
const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform());
94+
95+
logger.debug('Configuring local web server identity');
9396
const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection);
9497
const ldpServerToken = appServerIdentity.identityToken;
9598
const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username];

src/commands/lightning/dev/site.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ export default class LightningDevSite extends SfCommand<void> {
3737
const org = flags['target-org'];
3838
let siteName = flags.name;
3939

40-
const localDevEnabled = await OrgUtils.isLocalDevEnabled(org.getConnection(undefined));
40+
const connection = org.getConnection(undefined);
41+
42+
const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection);
4143
if (!localDevEnabled) {
4244
throw new Error(sharedMessages.getMessage('error.localdev.not.enabled'));
4345
}
4446

47+
OrgUtils.ensureMatchingAPIVersion(connection);
48+
4549
// If user doesn't specify a site, prompt the user for one
4650
if (!siteName) {
4751
const allSites = await ExperienceSite.getAllExpSites(org);

src/shared/orgUtils.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
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 { Connection } from '@salesforce/core';
8+
import path from 'node:path';
9+
import url from 'node:url';
10+
import { Connection, Messages } from '@salesforce/core';
11+
import { CommonUtils, Version } from '@salesforce/lwc-dev-mobile-core';
12+
13+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
14+
const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils');
915

1016
type LightningPreviewMetadataResponse = {
1117
enableLightningPreviewPref?: string;
@@ -18,6 +24,39 @@ export type AppDefinition = {
1824
DurableId: string;
1925
};
2026

27+
/**
28+
* As we go through different phases of release cycles, in order to ensure that the API version supported by
29+
* the local dev server matches with Org API versions, we rely on defining a metadata section in package.json
30+
*
31+
* The "apiVersionMetadata" entry in this json file defines "target" and "versionToTagMappings" sections.
32+
*
33+
* "target.versionNumber" defines the API version that the local dev server supports. As we pull in new versions
34+
* of the lwc-dev-server we need to manually update "target.versionNumber" in package.json In order to ensure
35+
* that we don't forget this step, we also have "target.matchingDevServerVersion" which is used by husky during
36+
* the pre-commit check to ensure that we have updated the "apiVersionMetadata" section. Whenever we pull in
37+
* a new version of lwc-dev-server in our dependencies, we must also update "target.matchingDevServerVersion"
38+
* to the same version otherwise the pre-commit will fail. This means that, as the PR owner deliberately
39+
* updates "target.matchingDevServerVersion", they are responsible to ensuring that the rest of the data under
40+
* "apiVersionMetadata" is accurate.
41+
*
42+
* The "versionToTagMappings" section will provide a mapping between supported API version by the dev server
43+
* and the tagged version of our plugin. We use "versionToTagMappings" to convey to the user which version of
44+
* our plugin should they be using to match with the API version of their org (i.e which version of our plugin
45+
* contains the lwc-dev-server dependency that can support the API version of their org).
46+
*/
47+
type ApiVersionMetadata = {
48+
target: {
49+
versionNumber: string;
50+
matchingDevServerVersion: string;
51+
};
52+
versionToTagMappings: [
53+
{
54+
versionNumber: string;
55+
tagName: string;
56+
}
57+
];
58+
};
59+
2160
export class OrgUtils {
2261
/**
2362
* Given an app name, it queries the AppDefinition table in the org to find
@@ -61,7 +100,7 @@ export class OrgUtils {
61100
const results: AppDefinition[] = [];
62101

63102
const appMenuItemsQuery =
64-
'SELECT Label,Description,Name FROM AppMenuItem WHERE IsAccessible=true AND IsVisible=TRUE';
103+
'SELECT Label,Description,Name FROM AppMenuItem WHERE IsAccessible=true AND IsVisible=true';
65104
const appMenuItems = await connection.query<{ Label: string; Description: string; Name: string }>(
66105
appMenuItemsQuery
67106
);
@@ -112,4 +151,47 @@ export class OrgUtils {
112151
}
113152
throw new Error('Could not save the app server identity token to the org.');
114153
}
154+
155+
/**
156+
* Given a connection to an Org, it ensures that org API version matches what the local dev server expects.
157+
* To do this, it compares the org API version with the meta data stored in package.json under apiVersionMetadata.
158+
* If the API versions do not match then this method will throw an exception.
159+
*
160+
* @param connection the connection to the org
161+
*/
162+
public static ensureMatchingAPIVersion(connection: Connection): void {
163+
const dirname = path.dirname(url.fileURLToPath(import.meta.url));
164+
const packageJsonFilePath = path.resolve(dirname, '../../package.json');
165+
166+
const pkg = CommonUtils.loadJsonFromFile(packageJsonFilePath) as {
167+
name: string;
168+
apiVersionMetadata: ApiVersionMetadata;
169+
};
170+
const targetVersion = pkg.apiVersionMetadata.target.versionNumber;
171+
const orgVersion = connection.version;
172+
173+
if (Version.same(orgVersion, targetVersion) === false) {
174+
let errorMessage = messages.getMessage('error.org.api-mismatch.message', [orgVersion, targetVersion]);
175+
const tagName = pkg.apiVersionMetadata.versionToTagMappings.find(
176+
(info) => info.versionNumber === targetVersion
177+
)?.tagName;
178+
if (tagName) {
179+
const remediation = messages.getMessage('error.org.api-mismatch.remediation', [
180+
tagName,
181+
`${pkg.name}@${tagName}`,
182+
]);
183+
errorMessage = `${errorMessage} ${remediation}`;
184+
}
185+
186+
// Examples of error messages are as below (where the tag name comes from apiVersionMetadata in package.json):
187+
//
188+
// Your org is on API version 61.0, but this version of the CLI plugin supports API version 62.0. To use the plugin with this org, you can reinstall or update the plugin using the "latest" tag. For example: "sf plugins install @salesforce/plugin-lightning-dev@latest".
189+
//
190+
// Your org is on API version 62.0, but this version of the CLI plugin supports API version 63.0. To use the plugin with this org, you can reinstall or update the plugin using the "next" tag. For example: "sf plugins install @salesforce/plugin-lightning-dev@next".
191+
//
192+
// Your org is on API version 63.0, but this version of the CLI plugin supports API version 62.0. To use the plugin with this org, you can reinstall or update the plugin using the "latest" tag. For example: "sf plugins install @salesforce/plugin-lightning-dev@latest".
193+
194+
throw new Error(errorMessage);
195+
}
196+
}
115197
}

test/commands/lightning/dev/app.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('lightning dev app', () => {
9393
$$.SANDBOX.stub(Connection.prototype, 'getUsername').returns(testUsername);
9494
$$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(testIdentityData);
9595
$$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true);
96+
$$.SANDBOX.stub(OrgUtils, 'ensureMatchingAPIVersion').returns();
9697

9798
MockedLightningPreviewApp = await esmock<typeof LightningDevApp>('../../../../src/commands/lightning/dev/app.js', {
9899
'../../../../src/lwc-dev-server/index.js': {

0 commit comments

Comments
 (0)