From 88f36c0a7e5ddbf0af4c030bd654e9e079e20f16 Mon Sep 17 00:00:00 2001 From: maliroteh-sf <65030660+maliroteh-sf@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:40:36 -0700 Subject: [PATCH] feat: add api version validation (#218) * feat: add api version validation * chore: update comment * chore: use ESM instead of CJS * chore: address feedback --- .husky/check-versions.js | 23 +++++++ .husky/pre-commit | 3 + messages/shared.utils.md | 8 +++ package.json | 17 +++++ src/commands/lightning/dev/app.ts | 7 +- src/commands/lightning/dev/site.ts | 6 +- src/shared/orgUtils.ts | 86 ++++++++++++++++++++++++- test/commands/lightning/dev/app.test.ts | 1 + 8 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 .husky/check-versions.js diff --git a/.husky/check-versions.js b/.husky/check-versions.js new file mode 100644 index 0000000..ae8adc3 --- /dev/null +++ b/.husky/check-versions.js @@ -0,0 +1,23 @@ +import fs from 'node:fs'; + +// Read package.json +const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); + +// Extract versions +const devServerDependencyVersion = packageJson.dependencies['@lwc/lwc-dev-server']; +const devServerTargetVersion = packageJson.apiVersionMetadata?.target?.matchingDevServerVersion; + +if (!devServerDependencyVersion || !devServerTargetVersion) { + console.error('Error: missing @lwc/lwc-dev-server or matchingDevServerVersion'); + process.exit(1); // Fail the check +} + +// Compare versions +if (devServerDependencyVersion === devServerTargetVersion) { + process.exit(0); // Pass the check +} else { + console.error( + `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.` + ); + process.exit(1); // Fail the check +} diff --git a/.husky/pre-commit b/.husky/pre-commit index 4fbfe02..26e6a54 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,7 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +# Run the custom version check script +node .husky/check-versions.js + yarn lint && yarn pretty-quick --staged diff --git a/messages/shared.utils.md b/messages/shared.utils.md index a7d4ffe..4137631 100644 --- a/messages/shared.utils.md +++ b/messages/shared.utils.md @@ -29,3 +29,11 @@ You must provide valid SSL certificate data # error.localdev.not.enabled 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. + +# error.org.api-mismatch.message + +Your org is on API version %s, but this version of the CLI plugin supports API version %s. + +# error.org.api-mismatch.remediation + +To use the plugin with this org, you can reinstall or update the plugin using the "%s" tag. For example: "sf plugins install %s". diff --git a/package.json b/package.json index 342caa0..31b3a27 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,23 @@ "output": [] } }, + "apiVersionMetadata": { + "comment": "Refer to ApiVersionMetadata in orgUtils.ts for details", + "target": { + "versionNumber": "62.0", + "matchingDevServerVersion": "^9.5.1" + }, + "versionToTagMappings": [ + { + "versionNumber": "62.0", + "tagName": "latest" + }, + { + "versionNumber": "63.0", + "tagName": "next" + } + ] + }, "exports": "./lib/index.js", "type": "module", "volta": { diff --git a/src/commands/lightning/dev/app.ts b/src/commands/lightning/dev/app.ts index ba232b3..48352de 100644 --- a/src/commands/lightning/dev/app.ts +++ b/src/commands/lightning/dev/app.ts @@ -68,7 +68,6 @@ export default class LightningDevApp extends SfCommand { const targetOrg = flags['target-org']; const appName = flags['name']; - const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform()); const deviceId = flags['device-id']; let sfdxProjectRootPath = ''; @@ -78,7 +77,6 @@ export default class LightningDevApp extends SfCommand { return Promise.reject(new Error(messages.getMessage('error.no-project', [(error as Error)?.message ?? '']))); } - logger.debug('Configuring local web server identity'); const connection = targetOrg.getConnection(undefined); const username = connection.getUsername(); if (!username) { @@ -90,6 +88,11 @@ export default class LightningDevApp extends SfCommand { return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled'))); } + OrgUtils.ensureMatchingAPIVersion(connection); + + const platform = flags['device-type'] ?? (await PromptUtils.promptUserToSelectPlatform()); + + logger.debug('Configuring local web server identity'); const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection); const ldpServerToken = appServerIdentity.identityToken; const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username]; diff --git a/src/commands/lightning/dev/site.ts b/src/commands/lightning/dev/site.ts index bb55d77..0e94732 100644 --- a/src/commands/lightning/dev/site.ts +++ b/src/commands/lightning/dev/site.ts @@ -37,11 +37,15 @@ export default class LightningDevSite extends SfCommand { const org = flags['target-org']; let siteName = flags.name; - const localDevEnabled = await OrgUtils.isLocalDevEnabled(org.getConnection(undefined)); + const connection = org.getConnection(undefined); + + const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection); if (!localDevEnabled) { throw new Error(sharedMessages.getMessage('error.localdev.not.enabled')); } + OrgUtils.ensureMatchingAPIVersion(connection); + // If user doesn't specify a site, prompt the user for one if (!siteName) { const allSites = await ExperienceSite.getAllExpSites(org); diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 1bd4007..c97ecbd 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -5,7 +5,13 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { Connection } from '@salesforce/core'; +import path from 'node:path'; +import url from 'node:url'; +import { Connection, Messages } from '@salesforce/core'; +import { CommonUtils, Version } from '@salesforce/lwc-dev-mobile-core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils'); type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -18,6 +24,39 @@ export type AppDefinition = { DurableId: string; }; +/** + * As we go through different phases of release cycles, in order to ensure that the API version supported by + * the local dev server matches with Org API versions, we rely on defining a metadata section in package.json + * + * The "apiVersionMetadata" entry in this json file defines "target" and "versionToTagMappings" sections. + * + * "target.versionNumber" defines the API version that the local dev server supports. As we pull in new versions + * of the lwc-dev-server we need to manually update "target.versionNumber" in package.json In order to ensure + * that we don't forget this step, we also have "target.matchingDevServerVersion" which is used by husky during + * the pre-commit check to ensure that we have updated the "apiVersionMetadata" section. Whenever we pull in + * a new version of lwc-dev-server in our dependencies, we must also update "target.matchingDevServerVersion" + * to the same version otherwise the pre-commit will fail. This means that, as the PR owner deliberately + * updates "target.matchingDevServerVersion", they are responsible to ensuring that the rest of the data under + * "apiVersionMetadata" is accurate. + * + * The "versionToTagMappings" section will provide a mapping between supported API version by the dev server + * and the tagged version of our plugin. We use "versionToTagMappings" to convey to the user which version of + * our plugin should they be using to match with the API version of their org (i.e which version of our plugin + * contains the lwc-dev-server dependency that can support the API version of their org). + */ +type ApiVersionMetadata = { + target: { + versionNumber: string; + matchingDevServerVersion: string; + }; + versionToTagMappings: [ + { + versionNumber: string; + tagName: string; + } + ]; +}; + export class OrgUtils { /** * Given an app name, it queries the AppDefinition table in the org to find @@ -61,7 +100,7 @@ export class OrgUtils { const results: AppDefinition[] = []; const appMenuItemsQuery = - 'SELECT Label,Description,Name FROM AppMenuItem WHERE IsAccessible=true AND IsVisible=TRUE'; + 'SELECT Label,Description,Name FROM AppMenuItem WHERE IsAccessible=true AND IsVisible=true'; const appMenuItems = await connection.query<{ Label: string; Description: string; Name: string }>( appMenuItemsQuery ); @@ -112,4 +151,47 @@ export class OrgUtils { } throw new Error('Could not save the app server identity token to the org.'); } + + /** + * Given a connection to an Org, it ensures that org API version matches what the local dev server expects. + * To do this, it compares the org API version with the meta data stored in package.json under apiVersionMetadata. + * If the API versions do not match then this method will throw an exception. + * + * @param connection the connection to the org + */ + public static ensureMatchingAPIVersion(connection: Connection): void { + const dirname = path.dirname(url.fileURLToPath(import.meta.url)); + const packageJsonFilePath = path.resolve(dirname, '../../package.json'); + + const pkg = CommonUtils.loadJsonFromFile(packageJsonFilePath) as { + name: string; + apiVersionMetadata: ApiVersionMetadata; + }; + const targetVersion = pkg.apiVersionMetadata.target.versionNumber; + const orgVersion = connection.version; + + if (Version.same(orgVersion, targetVersion) === false) { + let errorMessage = messages.getMessage('error.org.api-mismatch.message', [orgVersion, targetVersion]); + const tagName = pkg.apiVersionMetadata.versionToTagMappings.find( + (info) => info.versionNumber === targetVersion + )?.tagName; + if (tagName) { + const remediation = messages.getMessage('error.org.api-mismatch.remediation', [ + tagName, + `${pkg.name}@${tagName}`, + ]); + errorMessage = `${errorMessage} ${remediation}`; + } + + // Examples of error messages are as below (where the tag name comes from apiVersionMetadata in package.json): + // + // 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". + // + // 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". + // + // 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". + + throw new Error(errorMessage); + } + } } diff --git a/test/commands/lightning/dev/app.test.ts b/test/commands/lightning/dev/app.test.ts index f116773..a31c516 100644 --- a/test/commands/lightning/dev/app.test.ts +++ b/test/commands/lightning/dev/app.test.ts @@ -93,6 +93,7 @@ describe('lightning dev app', () => { $$.SANDBOX.stub(Connection.prototype, 'getUsername').returns(testUsername); $$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(testIdentityData); $$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true); + $$.SANDBOX.stub(OrgUtils, 'ensureMatchingAPIVersion').returns(); MockedLightningPreviewApp = await esmock('../../../../src/commands/lightning/dev/app.js', { '../../../../src/lwc-dev-server/index.js': {