From 4e42a9d79316543ca342d63fbc87db38a48a28fb Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Tue, 13 Jan 2026 14:53:26 -0500 Subject: [PATCH 01/10] feat: use the right LWC/Dev server version for the org --- DUAL_VERSION.md | 1103 +++++++++++++++++++++++ messages/lightning.dev.app.md | 8 + messages/lightning.dev.component.md | 8 + messages/lightning.dev.site.md | 8 + package.json | 47 +- src/commands/lightning/dev/app.ts | 69 +- src/commands/lightning/dev/component.ts | 21 +- src/commands/lightning/dev/site.ts | 44 +- src/lwc-dev-server/index.ts | 20 +- src/shared/dependencyLoader.ts | 111 +++ src/shared/orgUtils.ts | 126 ++- src/shared/previewUtils.ts | 11 +- src/shared/promptUtils.ts | 6 +- src/shared/versionResolver.ts | 183 ++++ src/types/aliased-deps.d.ts | 51 ++ test/commands/lightning/dev/app.test.ts | 2 + test/shared/dependencyLoader.test.ts | 51 ++ test/shared/orgUtils.test.ts | 71 ++ test/shared/previewUtils.test.ts | 13 +- test/shared/versionResolver.test.ts | 112 +++ yarn.lock | 702 ++++++++++++++- 21 files changed, 2657 insertions(+), 110 deletions(-) create mode 100644 DUAL_VERSION.md create mode 100644 src/shared/dependencyLoader.ts create mode 100644 src/shared/versionResolver.ts create mode 100644 src/types/aliased-deps.d.ts create mode 100644 test/shared/dependencyLoader.test.ts create mode 100644 test/shared/versionResolver.test.ts diff --git a/DUAL_VERSION.md b/DUAL_VERSION.md new file mode 100644 index 00000000..7aaedf06 --- /dev/null +++ b/DUAL_VERSION.md @@ -0,0 +1,1103 @@ +# Design Proposal: Dual Version Support via NPM Aliasing + +## Problem Statement + +Currently, users must install the specific version of `@salesforce/plugin-lightning-dev` that matches their org's API version. This is because the plugin depends on specific versions of: + +- `@lwc/lwc-dev-server` +- `@lwc/sfdc-lwc-compiler` +- `lwc` + +These dependencies must match (at least in major/minor versions) the LWC runtime version running in the user's org. Since only 2 Salesforce org versions exist at any given time (Latest and Prerelease), we can support both simultaneously in a single plugin installation using NPM aliasing. + +## Goals + +1. **Eliminate version switching**: Users should be able to use a single plugin installation for both Latest and Prerelease orgs +2. **Automatic version detection**: Plugin should automatically determine which org version is being connected to +3. **Transparent operation**: User should not need to know or care about which dependency version is being used +4. **Maintainable**: Solution should be easy to update as new versions are released + +## Proposed Solution: NPM Aliasing with Runtime Branching + +### Overview + +Use NPM package aliasing to install two versions of each critical dependency side-by-side, then dynamically import the correct version at runtime based on the detected org API version. + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User Command │ +│ sf lightning dev component │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Org Connection Established │ +│ OrgUtils.getOrgAPIVersion(connection) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Version Resolution (NEW) │ +│ VersionResolver.resolveVersionChannel(apiVersion) │ +│ Returns: 'latest' | 'prerelease' │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Dynamic Dependency Loading (NEW) │ +│ DependencyLoader.loadLwcServer(channel) │ +│ Imports from: @lwc/lwc-dev-server-{channel} │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ LWC Server Started │ +│ Using correct version for org │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Implementation Details + +### 1. NPM Aliasing in package.json + +Update the dependencies section to include aliased versions: + +```json +{ + "dependencies": { + // Latest release versions (e.g., API 64.0) + "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.1.x", + "@lwc/sfdc-lwc-compiler-latest": "npm:@lwc/sfdc-lwc-compiler@~13.1.x", + "lwc-latest": "npm:lwc@~8.22.x", + + // Prerelease versions (e.g., API 65.0) + "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.2.x", + "@lwc/sfdc-lwc-compiler-prerelease": "npm:@lwc/sfdc-lwc-compiler@~13.2.x", + "lwc-prerelease": "npm:lwc@~8.23.x", + + // Other dependencies remain unchanged + "@inquirer/prompts": "^5.3.8" + // ... etc + } +} +``` + +**Key Points:** + +- Package aliases use a `-latest` and `-prerelease` suffix +- The actual package versions are specified after `npm:` +- Both versions are always installed in `node_modules` + +### 2. Enhanced API Version Metadata + +Expand the `apiVersionMetadata` in `package.json` to include channel information: + +```json +{ + "apiVersionMetadata": { + "channels": { + "latest": { + "supportedApiVersions": ["64.0"], + "dependencies": { + "@lwc/lwc-dev-server": "~13.1.x", + "@lwc/sfdc-lwc-compiler": "~13.1.x", + "lwc": "~8.22.x" + } + }, + "prerelease": { + "supportedApiVersions": ["65.0", "66.0"], + "dependencies": { + "@lwc/lwc-dev-server": "~13.2.x", + "@lwc/sfdc-lwc-compiler": "~13.2.x", + "lwc": "~8.23.x" + } + } + }, + "defaultChannel": "latest", + "versionToTagMappings": [ + // Keep existing mappings for backward compatibility docs + ] + } +} +``` + +### 3. New Module: VersionResolver + +Create `src/shared/versionResolver.ts`: + +```typescript +/** + * Resolves org API version to appropriate dependency channel + */ +export type VersionChannel = 'latest' | 'prerelease'; + +export interface ChannelConfig { + supportedApiVersions: string[]; + dependencies: { + [key: string]: string; + }; +} + +export class VersionResolver { + private static channelMetadata: Map | null = null; + + /** + * Loads channel metadata from package.json + */ + private static loadChannelMetadata(): Map { + if (this.channelMetadata) { + return this.channelMetadata; + } + + const packageJson = this.getPackageJson(); + const channels = packageJson.apiVersionMetadata.channels; + + this.channelMetadata = new Map(); + for (const [channel, config] of Object.entries(channels)) { + this.channelMetadata.set(channel as VersionChannel, config as ChannelConfig); + } + + return this.channelMetadata; + } + + /** + * Given an org API version, returns the appropriate channel + * + * @param orgApiVersion - The API version from the org (e.g., "65.0") + * @returns The channel to use ('latest' or 'prerelease') + * @throws Error if the API version is not supported by any channel + */ + public static resolveChannel(orgApiVersion: string): VersionChannel { + const channels = this.loadChannelMetadata(); + + for (const [channel, config] of channels.entries()) { + if (config.supportedApiVersions.includes(orgApiVersion)) { + return channel; + } + } + + // If no exact match, try to find by major.minor comparison + const orgMajorMinor = this.getMajorMinor(orgApiVersion); + for (const [channel, config] of channels.entries()) { + for (const supportedVersion of config.supportedApiVersions) { + if (this.getMajorMinor(supportedVersion) === orgMajorMinor) { + return channel; + } + } + } + + throw new Error( + `Unsupported org API version: ${orgApiVersion}. ` + `This plugin supports: ${this.getSupportedVersionsList()}` + ); + } + + /** + * Extracts major.minor from a version string (e.g., "65.0" from "65.0.1") + */ + private static getMajorMinor(version: string): string { + const parts = version.split('.'); + return `${parts[0]}.${parts[1]}`; + } + + /** + * Returns a formatted list of all supported API versions + */ + private static getSupportedVersionsList(): string { + const channels = this.loadChannelMetadata(); + const allVersions: string[] = []; + + for (const config of channels.values()) { + allVersions.push(...config.supportedApiVersions); + } + + return allVersions.join(', '); + } + + /** + * Returns the default channel from package.json + */ + public static getDefaultChannel(): VersionChannel { + const packageJson = this.getPackageJson(); + return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; + } + + private static getPackageJson(): any { + // Implementation similar to OrgUtils.ensureMatchingAPIVersion + const dirname = path.dirname(url.fileURLToPath(import.meta.url)); + const packageJsonFilePath = path.resolve(dirname, '../../package.json'); + return CommonUtils.loadJsonFromFile(packageJsonFilePath); + } +} +``` + +### 4. New Module: DependencyLoader + +Create `src/shared/dependencyLoader.ts`: + +```typescript +import type { LWCServer, ServerConfig, Workspace } from '@lwc/lwc-dev-server'; +import type { VersionChannel } from './versionResolver.js'; + +/** + * Interface for dynamically loaded LWC server module + */ +interface LwcDevServerModule { + startLwcDevServer: (config: ServerConfig, logger: any) => Promise; + LWCServer: typeof LWCServer; + ServerConfig: typeof ServerConfig; + Workspace: typeof Workspace; +} + +/** + * Dynamically loads LWC dependencies based on version channel + */ +export class DependencyLoader { + private static loadedModules: Map = new Map(); + + /** + * Loads the LWC dev server module for the specified channel + * Uses dynamic import to load the aliased package at runtime + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded module + */ + public static async loadLwcDevServer(channel: VersionChannel): Promise { + // Check cache first + if (this.loadedModules.has(channel)) { + return this.loadedModules.get(channel)!; + } + + // Construct the aliased package name + const packageName = `@lwc/lwc-dev-server-${channel}`; + + try { + // Dynamic import of the aliased package + const module = (await import(packageName)) as LwcDevServerModule; + this.loadedModules.set(channel, module); + return module; + } catch (error) { + throw new Error( + `Failed to load LWC dev server for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Loads the LWC compiler module for the specified channel + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded compiler module + */ + public static async loadLwcCompiler(channel: VersionChannel): Promise { + const packageName = `@lwc/sfdc-lwc-compiler-${channel}`; + + try { + return await import(packageName); + } catch (error) { + throw new Error( + `Failed to load LWC compiler for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Loads the base LWC module for the specified channel + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded LWC module + */ + public static async loadLwc(channel: VersionChannel): Promise { + const packageName = `lwc-${channel}`; + + try { + return await import(packageName); + } catch (error) { + throw new Error( + `Failed to load LWC for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Clears the module cache (useful for testing) + */ + public static clearCache(): void { + this.loadedModules.clear(); + } +} +``` + +### 5. Updated OrgUtils + +Modify `src/shared/orgUtils.ts` to replace `ensureMatchingAPIVersion`: + +```typescript +/** + * Determines the version channel for the connected org + * + * @param connection - The connection to the org + * @param overrideChannel - Optional manual override from flag or env var + * @returns The version channel to use for dependencies + * @throws Error if the org version is not supported or invalid override provided + */ +public static getVersionChannel( + connection: Connection, + overrideChannel?: VersionChannel +): VersionChannel { + // Priority 1: Explicit override parameter (from --version-channel flag) + if (overrideChannel) { + Messages.getInstance().log(`Using manually specified version channel: ${overrideChannel}`); + return overrideChannel; + } + + // Priority 2: Environment variable override + const envOverride = process.env.FORCE_VERSION_CHANNEL; + if (envOverride) { + const validChannels: VersionChannel[] = ['latest', 'prerelease']; + if (validChannels.includes(envOverride as VersionChannel)) { + Messages.getInstance().log( + `Using version channel from FORCE_VERSION_CHANNEL: ${envOverride}` + ); + return envOverride as VersionChannel; + } else { + throw new Error( + `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + + `Valid values are: ${validChannels.join(', ')}` + ); + } + } + + // Priority 3: Skip check for testing (legacy compatibility) + if (process.env.SKIP_API_VERSION_CHECK === 'true') { + return VersionResolver.getDefaultChannel(); + } + + // Priority 4: Automatic detection based on org version + const orgVersion = connection.version; + + try { + const channel = VersionResolver.resolveChannel(orgVersion); + Messages.getInstance().log( + `Auto-detected version channel '${channel}' for org API version ${orgVersion}` + ); + return channel; + } catch (error) { + // Enhance error with helpful message + throw new Error( + `${error instanceof Error ? error.message : String(error)}\n` + + `Your org is on API version ${orgVersion}. ` + + `Please ensure you are using the correct version of the CLI and this plugin.` + ); + } +} + +// Keep the old method but mark as deprecated for now +/** @deprecated Use getVersionChannel instead */ +public static ensureMatchingAPIVersion(connection: Connection): void { + // Implementation can call getVersionChannel and throw if it fails + this.getVersionChannel(connection); +} +``` + +### 6. Updated LWC Server Initialization + +Modify `src/lwc-dev-server/index.ts`: + +```typescript +import { Connection } from '@salesforce/core'; +import { OrgUtils } from '../shared/orgUtils.js'; +import { DependencyLoader } from '../shared/dependencyLoader.js'; +import type { VersionChannel } from '../shared/versionResolver.js'; + +export async function startLWCServer( + logger: Logger, + connection: Connection, // NEW: pass connection to determine version + rootDir: string, + token: string, + clientType: string, + serverPorts?: { httpPort: number; httpsPort: number }, + certData?: SSLCertificateData, + workspace?: Workspace, + versionChannelOverride?: VersionChannel // NEW: optional manual override +): Promise { + // NEW: Determine which version channel to use (with optional override) + const channel: VersionChannel = OrgUtils.getVersionChannel(connection, versionChannelOverride); + logger.trace(`Using version channel: ${channel}`); + + // NEW: Load the appropriate version of the dev server + const lwcDevServerModule = await DependencyLoader.loadLwcDevServer(channel); + + const config = await createLWCServerConfig(rootDir, token, clientType, serverPorts, certData, workspace); + + logger.trace(`Starting LWC Dev Server with config: ${JSON.stringify(config)}`); + + // Use the dynamically loaded startLwcDevServer function + let lwcDevServer: LWCServer | null = await lwcDevServerModule.startLwcDevServer(config, logger); + + const cleanup = (): void => { + if (lwcDevServer) { + logger.trace('Stopping LWC Dev Server'); + lwcDevServer.stopServer(); + lwcDevServer = null; + } + }; + + [ + 'exit', // normal exit flow + 'SIGINT', // when a user presses ctrl+c + 'SIGTERM', // when a user kills the process + ].forEach((signal) => process.on(signal, cleanup)); + + return lwcDevServer; +} +``` + +### 7. Update Command Files + +Each command (app.ts, component.ts, site.ts) needs to: + +1. Add the `--version-channel` flag +2. Pass the connection to startLWCServer +3. Support manual override + +```typescript +// In src/commands/lightning/dev/component.ts (and similar for app.ts, site.ts) + +import { Flags } from '@oclif/core'; + +export default class Component extends SfCommand { + public static readonly flags = { + // ... existing flags + 'version-channel': Flags.string({ + summary: 'Manually specify which version channel to use (latest or prerelease)', + description: + 'Override automatic version detection and force a specific dependency channel. ' + + 'Useful for testing and debugging. Valid values: "latest", "prerelease".', + options: ['latest', 'prerelease'], + required: false, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(Component); + + // ... existing code to get connection + const conn = flags['target-org'].getConnection(); + + // Remove or comment out the old API version check + // OrgUtils.ensureMatchingAPIVersion(conn); + + // ... existing code + + // Pass connection and optional channel override to startLWCServer + const server = await startLWCServer( + this.logger, + conn, // NEW: pass connection + projectDir, + identityToken, + 'sfdx-component', + serverPorts, + certData, + workspace, + flags['version-channel'] as VersionChannel | undefined // NEW: pass override + ); + } +} +``` + +### 8. Optional: Version Channel Caching + +For performance optimization, implement simple caching to avoid repeatedly querying the org: + +```typescript +// In src/shared/versionResolver.ts + +interface CacheEntry { + apiVersion: string; + channel: VersionChannel; + timestamp: number; +} + +export class VersionResolver { + private static channelMetadata: Map | null = null; + private static versionCache: Map = new Map(); + private static readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + + /** + * Resolves channel with caching support + * + * @param orgId - Unique identifier for the org + * @param orgApiVersion - The API version from the org + * @returns The channel to use + */ + public static resolveChannelWithCache(orgId: string, orgApiVersion: string): VersionChannel { + // Check cache first + const cached = this.versionCache.get(orgId); + if (cached) { + const age = Date.now() - cached.timestamp; + if (age < this.CACHE_TTL_MS && cached.apiVersion === orgApiVersion) { + return cached.channel; + } + // Cache expired or version changed, remove it + this.versionCache.delete(orgId); + } + + // Resolve and cache + const channel = this.resolveChannel(orgApiVersion); + this.versionCache.set(orgId, { + apiVersion: orgApiVersion, + channel, + timestamp: Date.now(), + }); + + return channel; + } + + /** + * Clears the version cache (useful for testing or when orgs are upgraded) + */ + public static clearCache(): void { + this.versionCache.clear(); + } + + /** + * Removes a specific org from the cache + */ + public static removeCacheEntry(orgId: string): void { + this.versionCache.delete(orgId); + } + + // ... rest of existing methods +} +``` + +**Usage in OrgUtils:** + +```typescript +public static getVersionChannel( + connection: Connection, + overrideChannel?: VersionChannel +): VersionChannel { + // ... existing override logic ... + + // Use cached resolution if available + const orgId = connection.getAuthInfoFields().orgId; + const orgVersion = connection.version; + + const channel = VersionResolver.resolveChannelWithCache(orgId, orgVersion); + // ... rest of logic +} +``` + +**Note**: This caching is optional and can be added in a follow-up iteration if performance testing shows it's needed. + +## TypeScript Considerations + +### Type Definitions + +Since we're using aliased packages, TypeScript needs to know about them: + +**Option A: Use `declare module` (Simpler)** + +Create `src/types/aliased-deps.d.ts`: + +```typescript +// Declare aliased LWC dev server packages as having same types as base package +declare module '@lwc/lwc-dev-server-latest' { + export * from '@lwc/lwc-dev-server'; +} + +declare module '@lwc/lwc-dev-server-prerelease' { + export * from '@lwc/lwc-dev-server'; +} + +declare module '@lwc/sfdc-lwc-compiler-latest' { + export * from '@lwc/sfdc-lwc-compiler'; +} + +declare module '@lwc/sfdc-lwc-compiler-prerelease' { + export * from '@lwc/sfdc-lwc-compiler'; +} + +declare module 'lwc-latest' { + export * from 'lwc'; +} + +declare module 'lwc-prerelease' { + export * from 'lwc'; +} +``` + +**Option B: Use TypeScript Path Mapping (More explicit)** + +In `tsconfig.json`: + +```json +{ + "compilerOptions": { + "paths": { + "@lwc/lwc-dev-server-latest": ["./node_modules/@lwc/lwc-dev-server"], + "@lwc/lwc-dev-server-prerelease": ["./node_modules/@lwc/lwc-dev-server"], + "@lwc/sfdc-lwc-compiler-latest": ["./node_modules/@lwc/sfdc-lwc-compiler"], + "@lwc/sfdc-lwc-compiler-prerelease": ["./node_modules/@lwc/sfdc-lwc-compiler"], + "lwc-latest": ["./node_modules/lwc"], + "lwc-prerelease": ["./node_modules/lwc"] + } + } +} +``` + +**Recommendation**: Use Option A (declare module) as it's simpler and doesn't affect the build output. + +## Testing Strategy + +### Unit Tests + +1. **VersionResolver Tests** (`test/shared/versionResolver.test.ts`): + + - Test channel resolution for each supported API version + - Test error handling for unsupported versions + - Test major.minor version matching + - Test default channel retrieval + +2. **DependencyLoader Tests** (`test/shared/dependencyLoader.test.ts`): + + - Test loading each channel's dependencies + - Test caching behavior + - Test error handling for missing packages + - Mock dynamic imports to avoid actual package loading + +3. **OrgUtils Tests** (update existing): + - Test `getVersionChannel` with various API versions + - Test with `SKIP_API_VERSION_CHECK` env var + +### Integration Tests (NUTs) + +1. **Dual Version Tests** (`test/commands/lightning/dev/dualVersion.nut.ts`): + + - Test against an org with API version 64.0 (should use 'latest') + - Test against an org with API version 65.0 (should use 'prerelease') + - Verify correct dependencies are loaded + - Verify dev server starts successfully with each version + +2. **Update Existing NUTs**: + - Ensure existing NUTs work without modification + - Add assertions to verify correct channel is being used + +### Manual Testing Checklist + +- [ ] Install plugin with both dependency versions +- [ ] Connect to a Latest org (e.g., API 64.0) and verify dev server starts +- [ ] Connect to a Prerelease org (e.g., API 65.0) and verify dev server starts +- [ ] Verify hot reload works with both versions +- [ ] Test component preview with both versions +- [ ] Test app preview with both versions +- [ ] Test site preview with both versions +- [ ] Test manual override flag: `--version-channel=latest` and `--version-channel=prerelease` +- [ ] Test environment variable override: `FORCE_VERSION_CHANNEL=latest` and `FORCE_VERSION_CHANNEL=prerelease` +- [ ] Verify that flag override takes precedence over env var +- [ ] Test invalid channel values and verify error messages + +### User Documentation for Manual Override + +**Using the `--version-channel` Flag:** + +For developers who need to test with a specific version or work around automatic detection: + +```bash +# Force use of latest channel +sf lightning dev component --target-org my-org --version-channel latest + +# Force use of prerelease channel +sf lightning dev app --target-org my-org --version-channel prerelease + +# Works with all lightning dev commands +sf lightning dev site --target-org my-org --version-channel latest +``` + +**Using the `FORCE_VERSION_CHANNEL` Environment Variable:** + +For persistent override during a development session: + +```bash +# Set environment variable (bash/zsh) +export FORCE_VERSION_CHANNEL=prerelease +sf lightning dev component --target-org my-org + +# Set for single command +FORCE_VERSION_CHANNEL=latest sf lightning dev component --target-org my-org +``` + +**Override Priority (highest to lowest):** + +1. `--version-channel` flag +2. `FORCE_VERSION_CHANNEL` environment variable +3. `SKIP_API_VERSION_CHECK=true` (legacy, returns default channel) +4. Automatic detection based on org API version + +**Use Cases for Manual Override:** + +- **Testing**: Test your components with different LWC runtime versions +- **Debugging**: Isolate whether an issue is version-specific +- **Preview Upgrades**: Test with prerelease version before org upgrade +- **Workarounds**: If automatic detection fails or org metadata is incorrect +- **Development**: Plugin developers testing against specific versions + +## Migration Strategy + +### Phase 1: Implementation (1-2 weeks) + +1. Add aliased dependencies to package.json +2. Implement VersionResolver and DependencyLoader modules +3. Update OrgUtils with new getVersionChannel method +4. Update lwc-dev-server/index.ts to use dynamic loading +5. Update command files to pass connection to startLWCServer +6. Add TypeScript type declarations for aliased packages +7. Write comprehensive unit tests + +### Phase 2: Integration & Testing (1 week) + +1. Write integration tests (NUTs) +2. Manual testing with both Latest and Prerelease orgs +3. Test edge cases and error scenarios +4. Performance testing to ensure no significant overhead + +### Phase 3: Documentation & Release (1 week) + +1. Update README with new capabilities +2. Update user-facing documentation +3. Update error messages to be more helpful +4. Create migration guide for users +5. Release as major version (breaking change in how versions are handled) + +### Phase 4: Cleanup (future release) + +1. Remove deprecated `ensureMatchingAPIVersion` method +2. Remove old version-specific error messages +3. Update `versionToTagMappings` metadata (may no longer be needed) + +## Rollout Plan + +### Option A: Big Bang (Recommended) + +- Release as a new major version (e.g., v6.0.0) +- Announce that users no longer need to switch plugin versions +- Provide clear upgrade instructions +- Keep v5.x as fallback for any issues + +### Option B: Gradual + +- Release as opt-in feature with flag (e.g., `--use-dual-version`) +- Gather feedback for 1-2 releases +- Make it default behavior in next major version + +**Recommendation**: Option A - The change is transparent to users and provides immediate value. + +## Maintenance Considerations + +### Updating Versions + +When a new Salesforce release comes out: + +1. **Update package.json dependencies**: + + ```json + { + "dependencies": { + // Shift prerelease to latest + "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.2.x", + + // Update prerelease to new version + "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x" + } + } + ``` + +2. **Update apiVersionMetadata**: + + ```json + { + "apiVersionMetadata": { + "channels": { + "latest": { + "supportedApiVersions": ["65.0"] // Previous prerelease becomes latest + }, + "prerelease": { + "supportedApiVersions": ["66.0"] // New prerelease version + } + } + } + } + ``` + +3. **Run tests** against both versions +4. **Release** new version of plugin + +### Automation Opportunities + +- Create a script to update versions in package.json +- Automate the shifting of prerelease → latest +- Set up CI/CD to test against both org types +- Create alerts when new LWC package versions are published + +### Extending to 3 Channels (Future) + +If needed, the design can easily support 3 channels. Here's how: + +**1. Update package.json:** + +```json +{ + "dependencies": { + // Previous release (n-2) + "@lwc/lwc-dev-server-previous": "npm:@lwc/lwc-dev-server@~13.1.x", + + // Latest release (n-1) + "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.2.x", + + // Prerelease (n) + "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x" + } +} +``` + +**2. Update apiVersionMetadata:** + +```json +{ + "apiVersionMetadata": { + "channels": { + "previous": { + "supportedApiVersions": ["64.0"] + }, + "latest": { + "supportedApiVersions": ["65.0"] + }, + "prerelease": { + "supportedApiVersions": ["66.0"] + } + } + } +} +``` + +**3. Update VersionChannel type:** + +```typescript +export type VersionChannel = 'previous' | 'latest' | 'prerelease'; +``` + +**4. Update command flag options:** + +```typescript +'version-channel': Flags.string({ + options: ['previous', 'latest', 'prerelease'], + // ... +}) +``` + +**5. Update type declarations:** + +Add declarations for `@lwc/lwc-dev-server-previous`, `@lwc/sfdc-lwc-compiler-previous`, and `lwc-previous`. + +**When to use 3 channels:** + +- During transition periods when some orgs haven't upgraded yet +- When supporting extended compatibility windows +- For beta testing environments + +**Trade-offs:** + +- Increases bundle size by ~50% +- Slightly more complex maintenance +- More testing surface area + +The channel-based architecture makes this extension straightforward with minimal code changes. + +## Potential Issues & Mitigations + +### Issue 1: Type Compatibility + +**Problem**: Different versions might have incompatible TypeScript types + +**Mitigation**: + +- Use interface abstraction layer if needed +- Test compilation with both versions during CI +- Create adapter classes if APIs diverge significantly + +### Issue 2: Bundle Size + +**Problem**: Installing two versions doubles the size of LWC dependencies + +**Mitigation**: + +- These are dev dependencies, size is less critical +- Consider optional peer dependencies in future +- Monitor and document bundle size impact + +### Issue 3: Breaking Changes Between Versions + +**Problem**: Major version differences might require different code paths + +**Mitigation**: + +- Create version-specific adapters in DependencyLoader +- Use feature detection rather than version detection where possible +- Maintain compatibility layer + +### Issue 4: Debugging Complexity + +**Problem**: Bug reports might not specify which version was used + +**Mitigation**: + +- Log the channel being used in debug output +- Include channel in error messages +- Add channel info to telemetry (if applicable) + +### Issue 5: Development Environment + +**Problem**: Developers might need to test against specific versions + +**Mitigation**: + +- Add environment variable to force a specific channel (e.g., `FORCE_VERSION_CHANNEL=latest`) +- Add flag to command for testing (e.g., `--version-channel=prerelease`) +- Document development workflow + +## Alternative Approaches Considered + +### Alternative 1: Peer Dependencies + +Install dependencies as peer dependencies and let users install the correct version. + +**Pros**: Smaller plugin size, user has more control +**Cons**: Worse UX, users still need to know which version to install, defeats the purpose + +### Alternative 2: Separate Plugin Packages + +Create two separate plugins: `plugin-lightning-dev` and `plugin-lightning-dev-prerelease` + +**Pros**: Cleaner separation, no complexity in code +**Cons**: Terrible UX, users need to know which to install, doubles maintenance + +### Alternative 3: Lazy Installation + +Detect version and install correct dependencies at runtime via npm/yarn + +**Pros**: Only installs what's needed +**Cons**: Requires write access to node_modules, slow first run, complex error handling, security concerns + +### Alternative 4: Build-time Multiple Distributions + +Build two separate distributions of the plugin, one for each version + +**Pros**: No runtime overhead +**Cons**: Complex build process, doubles CI time, users still need to know which to install + +**Selected Approach**: NPM Aliasing with Runtime Branching (described above) + +- Best balance of UX and maintainability +- Transparent to users +- Manageable complexity +- Industry-standard approach + +## Success Metrics + +1. **User Experience**: Users can connect to any supported org without plugin version switching +2. **Performance**: < 100ms overhead for version resolution and dynamic loading +3. **Reliability**: No increase in error rates for dev server startup +4. **Maintenance**: Version updates can be done in < 30 minutes +5. **Adoption**: 90%+ of users upgrade to new version within 3 months + +## Decisions on Key Questions + +1. **Should we support more than 2 versions?** + + - **Decision**: Start with 2 versions (latest + prerelease) for initial implementation + - Architecture should be extensible to support 3 versions if needed in the future + - Using a channel-based approach makes this straightforward to extend + +2. **How do we handle version deprecation?** + + - **Decision**: Rotate versions with each Salesforce release cycle + - When a new release happens: prerelease → latest, new version → prerelease + - No need to maintain old versions since all orgs are upgraded + - Simple update process: update package.json dependencies and apiVersionMetadata + +3. **Should we cache the version resolution per org?** + + - **Decision**: Implement optional short-term caching + - Cache org API version in memory during command execution + - Consider adding a TTL-based cache (e.g., 5 minutes) to `.sf/` config to reduce org queries + - Cache key: org ID → { apiVersion, channel, timestamp } + - Keep implementation simple initially, can enhance if performance issues arise + +4. **Do we need a manual override flag?** + + - **Decision**: Yes, add manual override for testing and debugging + - Add flag: `--version-channel` to all commands + - Add environment variable: `FORCE_VERSION_CHANNEL` + - Useful for developers, testing, and troubleshooting edge cases + +5. **What about @lwrjs/api dependency?** + - **Decision**: No action needed - LWRJS is deprecated + - Functionality is being removed from sites + - Will be removed in future release, so don't invest in versioning it + - Keep current pinned version (0.18.3) for now + +## Next Steps + +1. **Review and iterate on this design** + + - Get feedback from team + - Address any concerns or questions + - Finalize approach + +2. **Create prototype** + + - Implement core VersionResolver and DependencyLoader + - Test dynamic loading with aliased packages + - Validate TypeScript compilation + +3. **Full implementation** + + - Follow migration strategy outlined above + - Comprehensive testing + - Documentation updates + +4. **Release and monitor** + - Release as new major version + - Monitor for issues + - Gather user feedback + +--- + +## Summary + +This design proposal introduces dual-version support for LWC dependencies using NPM aliasing and runtime branching. Key decisions have been finalized: + +✅ **Approved Approach**: NPM aliasing with 2 channels (latest + prerelease) +✅ **Extensibility**: Architecture supports 3+ channels if needed +✅ **Manual Override**: Flag (`--version-channel`) and environment variable (`FORCE_VERSION_CHANNEL`) +✅ **Caching**: Optional short-term caching with 5-minute TTL +✅ **Version Rotation**: Simple rotation model (prerelease → latest) with each release +✅ **LWRJS**: No versioning needed (deprecated, being removed) + +**Next Step**: Begin Phase 1 Implementation + +--- + +**Document Version**: 2.0 +**Last Updated**: 2025-11-05 +**Author**: Design Proposal +**Status**: ✅ Approved - Ready for Implementation diff --git a/messages/lightning.dev.app.md b/messages/lightning.dev.app.md index ce3765f9..58b6620d 100644 --- a/messages/lightning.dev.app.md +++ b/messages/lightning.dev.app.md @@ -31,6 +31,14 @@ Type of device to display the app preview. ID of the mobile device to display the preview if device type is set to `ios` or `android`. The default value is the ID of the first available mobile device. +# flags.version-channel.summary + +Manually specify which version channel to use (latest, prerelease, or next). + +# flags.version-channel.description + +Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". + # error.fetching.app-id Unable to determine App Id for %s diff --git a/messages/lightning.dev.component.md b/messages/lightning.dev.component.md index 6fc3aab0..0abc94a8 100644 --- a/messages/lightning.dev.component.md +++ b/messages/lightning.dev.component.md @@ -24,6 +24,14 @@ Name of a component to preview. Launch component preview without selecting a component +# flags.version-channel.summary + +Manually specify which version channel to use (latest, prerelease, or next). + +# flags.version-channel.description + +Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". + # error.directory Unable to find components diff --git a/messages/lightning.dev.site.md b/messages/lightning.dev.site.md index 20fd1011..9a0811b2 100644 --- a/messages/lightning.dev.site.md +++ b/messages/lightning.dev.site.md @@ -33,6 +33,14 @@ Preview the site as a guest user (rather than an authenticated user). Preview the SSR bundle +# flags.version-channel.summary + +Manually specify which version channel to use (latest, prerelease, or next). + +# flags.version-channel.description + +Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". + # examples - Select a site to preview from the org "myOrg": diff --git a/package.json b/package.json index fda58556..baafe273 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,13 @@ "@inquirer/prompts": "^5.3.8", "@inquirer/select": "^2.4.7", "@lwc/lwc-dev-server": "~13.3.8", + "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.2.x", + "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x", + "@lwc/lwc-dev-server-next": "npm:@lwc/lwc-dev-server@~13.3.x", "@lwc/sfdc-lwc-compiler": "~13.3.8", + "@lwc/sfdc-lwc-compiler-latest": "npm:@lwc/sfdc-lwc-compiler@~13.2.x", + "@lwc/sfdc-lwc-compiler-prerelease": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", + "@lwc/sfdc-lwc-compiler-next": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", "@lwrjs/api": "0.18.3", "@oclif/core": "^4.5.6", "@salesforce/core": "^8.24.0", @@ -18,6 +24,9 @@ "axios": "^1.13.2", "glob": "^13.0.0", "lwc": "~8.27.0", + "lwc-latest": "npm:lwc@~8.23.x", + "lwc-prerelease": "npm:lwc@~8.24.x", + "lwc-next": "npm:lwc@~8.24.x", "node-fetch": "^3.3.2", "open": "^10.2.0", "xml2js": "^0.6.2" @@ -230,11 +239,39 @@ } }, "apiVersionMetadata": { - "x-apiVersionMetadata-comments": [ - "Refer to ApiVersionMetadata in orgUtils.ts for more details.", - "The 'target' section defines the dev server version (matchingDevServerVersion) and the API version that it can support (versionNumber).", - "The 'versionToTagMappings' section defines the mapping between released tags for our CLI plugin and the org version that each tag supports." - ], + "channels": { + "latest": { + "supportedApiVersions": [ + "65.0" + ], + "dependencies": { + "@lwc/lwc-dev-server": "~13.2.x", + "@lwc/sfdc-lwc-compiler": "~13.2.x", + "lwc": "~8.23.x" + } + }, + "prerelease": { + "supportedApiVersions": [ + "66.0" + ], + "dependencies": { + "@lwc/lwc-dev-server": "~13.3.x", + "@lwc/sfdc-lwc-compiler": "~13.3.x", + "lwc": "~8.24.x" + } + }, + "next": { + "supportedApiVersions": [ + "67.0" + ], + "dependencies": { + "@lwc/lwc-dev-server": "~13.3.x", + "@lwc/sfdc-lwc-compiler": "~13.3.x", + "lwc": "~8.24.x" + } + } + }, + "defaultChannel": "latest", "target": { "versionNumber": "67.0", "matchingDevServerVersion": "~13.3.8" diff --git a/src/commands/lightning/dev/app.ts b/src/commands/lightning/dev/app.ts index 4b13b309..4158c285 100644 --- a/src/commands/lightning/dev/app.ts +++ b/src/commands/lightning/dev/app.ts @@ -15,7 +15,7 @@ */ import path from 'node:path'; -import { Logger, Messages, SfProject } from '@salesforce/core'; +import { Logger, Messages, SfProject, Org } from '@salesforce/core'; import { AndroidAppPreviewConfig, AndroidDevice, @@ -29,6 +29,8 @@ import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; import { PromptUtils } from '../../../shared/promptUtils.js'; +import { MetaUtils } from '../../../shared/metaUtils.js'; +import { VersionChannel } from '../../../shared/versionResolver.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app'); @@ -68,6 +70,12 @@ export default class LightningDevApp extends SfCommand { summary: messages.getMessage('flags.device-id.summary'), char: 'i', }), + 'version-channel': Flags.string({ + summary: messages.getMessage('flags.version-channel.summary'), + description: messages.getMessage('flags.version-channel.description'), + options: ['latest', 'prerelease', 'next'], + required: false, + }), }; public async run(): Promise { @@ -78,6 +86,16 @@ export default class LightningDevApp extends SfCommand { const appName = flags['name']; const deviceId = flags['device-id']; + // Auto enable local dev + if (process.env.AUTO_ENABLE_LOCAL_DEV === 'true') { + try { + await MetaUtils.ensureLightningPreviewEnabled(targetOrg.getConnection(undefined)); + await MetaUtils.ensureFirstPartyCookiesNotRequired(targetOrg.getConnection(undefined)); + } catch (error) { + this.log('Error autoenabling local dev', error); + } + } + let sfdxProjectRootPath = ''; try { sfdxProjectRootPath = await SfProject.resolveProjectPath(); @@ -100,18 +118,23 @@ export default class LightningDevApp extends SfCommand { const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPorts, logger); logger.debug(`Local Dev Server url is ${ldpServerUrl}`); + const versionChannel = flags['version-channel'] as VersionChannel | undefined; + if (platform === Platform.desktop) { await this.desktopPreview( + targetOrg, sfdxProjectRootPath, serverPorts, ldpServerToken, ldpServerId, ldpServerUrl, appId, - logger + logger, + versionChannel ); } else { await this.mobilePreview( + targetOrg, platform, sfdxProjectRootPath, serverPorts, @@ -121,25 +144,28 @@ export default class LightningDevApp extends SfCommand { appName, appId, deviceId, - logger + logger, + versionChannel ); } } private async desktopPreview( + org: Org, sfdxProjectRootPath: string, serverPorts: { httpPort: number; httpsPort: number }, ldpServerToken: string, ldpServerId: string, ldpServerUrl: string, appId: string | undefined, - logger: Logger + logger: Logger, + versionChannelOverride?: VersionChannel ): Promise { if (!appId) { logger.debug('No Lightning Experience application name provided.... using the default app instead.'); } - const targetOrg = PreviewUtils.getTargetOrgFromArguments(this.argv); + const targetOrgArg = PreviewUtils.getTargetOrgFromArguments(this.argv); if (ldpServerUrl.startsWith('wss')) { this.log(`\n${messages.getMessage('trust.local.dev.server')}`); @@ -149,17 +175,28 @@ export default class LightningDevApp extends SfCommand { ldpServerUrl, ldpServerId, appId, - targetOrg + targetOrgArg ); // Start the LWC Dev Server - await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, Platform.desktop, serverPorts); + await startLWCServer( + logger, + org.getConnection(undefined), + sfdxProjectRootPath, + ldpServerToken, + Platform.desktop, + serverPorts, + undefined, + undefined, + versionChannelOverride + ); // Open the browser and navigate to the right page await this.config.runCommand('org:open', launchArguments); } private async mobilePreview( + org: Org, platform: Platform.ios | Platform.android, sfdxProjectRootPath: string, serverPorts: { httpPort: number; httpsPort: number }, @@ -169,7 +206,8 @@ export default class LightningDevApp extends SfCommand { appName: string | undefined, appId: string | undefined, deviceId: string | undefined, - logger: Logger + logger: Logger, + versionChannelOverride?: VersionChannel ): Promise { try { // Verify that user environment is set up for mobile (i.e. has right tooling) @@ -248,7 +286,8 @@ export default class LightningDevApp extends SfCommand { this.spinner.start(messages.getMessage('spinner.extract.archive')); const outputDir = path.dirname(bundlePath); const finalBundlePath = path.join(outputDir, 'Chatter.app'); - await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger as any); this.spinner.stop(); bundlePath = finalBundlePath; } @@ -260,7 +299,17 @@ export default class LightningDevApp extends SfCommand { } // Start the LWC Dev Server - await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, platform, serverPorts, certData); + await startLWCServer( + logger, + org.getConnection(undefined), + sfdxProjectRootPath, + ldpServerToken, + platform, + serverPorts, + certData, + undefined, + versionChannelOverride + ); // Launch the native app for previewing (launchMobileApp will show its own spinner) // eslint-disable-next-line camelcase diff --git a/src/commands/lightning/dev/component.ts b/src/commands/lightning/dev/component.ts index 5f5e4974..76ae5eab 100644 --- a/src/commands/lightning/dev/component.ts +++ b/src/commands/lightning/dev/component.ts @@ -23,6 +23,7 @@ import { PromptUtils } from '../../../shared/promptUtils.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { MetaUtils } from '../../../shared/metaUtils.js'; +import { VersionChannel } from '../../../shared/versionResolver.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component'); @@ -54,6 +55,12 @@ export default class LightningDevComponent extends SfCommand { @@ -161,7 +168,19 @@ export default class LightningDevComponent extends SfCommand { summary: messages.getMessage('flags.ssr.summary'), default: false, }), + 'version-channel': Flags.string({ + summary: messages.getMessage('flags.version-channel.summary'), + description: messages.getMessage('flags.version-channel.description'), + options: ['latest', 'prerelease', 'next'], + required: false, + }), }; public async run(): Promise { @@ -67,12 +75,22 @@ export default class LightningDevSite extends SfCommand { const connection = org.getConnection(undefined); + // Auto enable local dev + if (process.env.AUTO_ENABLE_LOCAL_DEV === 'true') { + try { + await MetaUtils.ensureLightningPreviewEnabled(connection); + await MetaUtils.ensureFirstPartyCookiesNotRequired(connection); + } catch (error) { + this.log('Error autoenabling local dev', error); + } + } + const localDevEnabled = await OrgUtils.isLocalDevEnabled(connection); if (!localDevEnabled) { throw new Error(sharedMessages.getMessage('error.localdev.not.enabled')); } - OrgUtils.ensureMatchingAPIVersion(connection); + OrgUtils.getVersionChannel(connection, flags['version-channel'] as VersionChannel | undefined); // If user doesn't specify a site, prompt the user for one if (!siteName) { @@ -83,7 +101,11 @@ export default class LightningDevSite extends SfCommand { const selectedSite = new ExperienceSite(org, siteName); if (!ssr) { - return await this.openPreviewUrl(selectedSite, connection); + return await this.openPreviewUrl( + selectedSite, + connection, + flags['version-channel'] as VersionChannel | undefined + ); } await this.serveSSRSite(selectedSite, getLatest, siteName, guest); } catch (e) { @@ -152,7 +174,11 @@ export default class LightningDevSite extends SfCommand { } } - private async openPreviewUrl(selectedSite: ExperienceSite, connection: Connection): Promise { + private async openPreviewUrl( + selectedSite: ExperienceSite, + connection: Connection, + versionChannelOverride?: VersionChannel + ): Promise { let sfdxProjectRootPath = ''; try { sfdxProjectRootPath = await SfProject.resolveProjectPath(); @@ -182,7 +208,17 @@ export default class LightningDevSite extends SfCommand { this.log(`Local Dev Server url is ${ldpServerUrl}`); const logger = await Logger.child(this.ctor.name); - await startLWCServer(logger, sfdxProjectRootPath, ldpServerToken, Platform.desktop, serverPorts); + await startLWCServer( + logger, + connection, + sfdxProjectRootPath, + ldpServerToken, + Platform.desktop, + serverPorts, + undefined, + undefined, + versionChannelOverride + ); const url = new URL(previewUrl); url.searchParams.set('aura.ldpServerUrl', ldpServerUrl); url.searchParams.set('aura.ldpServerId', ldpServerId); diff --git a/src/lwc-dev-server/index.ts b/src/lwc-dev-server/index.ts index dada38e9..8f69e75c 100644 --- a/src/lwc-dev-server/index.ts +++ b/src/lwc-dev-server/index.ts @@ -15,10 +15,13 @@ */ import process from 'node:process'; -import { LWCServer, ServerConfig, startLwcDevServer, Workspace } from '@lwc/lwc-dev-server'; -import { Lifecycle, Logger, SfProject } from '@salesforce/core'; +import type { LWCServer, ServerConfig, Workspace } from '@lwc/lwc-dev-server'; +import { Connection, Lifecycle, Logger, SfProject } from '@salesforce/core'; import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; import { glob } from 'glob'; +import { DependencyLoader } from '../shared/dependencyLoader.js'; +import { OrgUtils } from '../shared/orgUtils.js'; +import { VersionChannel } from '../shared/versionResolver.js'; import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT, @@ -75,17 +78,24 @@ async function createLWCServerConfig( export async function startLWCServer( logger: Logger, + connection: Connection, rootDir: string, token: string, clientType: string, serverPorts?: { httpPort: number; httpsPort: number }, certData?: SSLCertificateData, - workspace?: Workspace + workspace?: Workspace, + versionChannelOverride?: VersionChannel ): Promise { + const channel = OrgUtils.getVersionChannel(connection, versionChannelOverride); + logger.trace(`Using version channel: ${channel}`); + + const lwcDevServerModule = await DependencyLoader.loadLwcDevServer(channel); + const config = await createLWCServerConfig(rootDir, token, clientType, serverPorts, certData, workspace); logger.trace(`Starting LWC Dev Server with config: ${JSON.stringify(config)}`); - let lwcDevServer: LWCServer | null = await startLwcDevServer(config, logger); + let lwcDevServer = (await lwcDevServerModule.startLwcDevServer(config, logger)) as LWCServer | null; const cleanup = (): void => { if (lwcDevServer) { @@ -101,5 +111,5 @@ export async function startLWCServer( 'SIGTERM', // when a user kills the process ].forEach((signal) => process.on(signal, cleanup)); - return lwcDevServer; + return lwcDevServer as LWCServer; } diff --git a/src/shared/dependencyLoader.ts b/src/shared/dependencyLoader.ts new file mode 100644 index 00000000..5cc30bbe --- /dev/null +++ b/src/shared/dependencyLoader.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Logger } from '@salesforce/core'; +import type { VersionChannel } from './versionResolver.js'; + +/** + * Type for dynamically loaded LWC server module + */ +export type LwcDevServerModule = { + startLwcDevServer: (config: unknown, logger: Logger) => Promise; + LWCServer: unknown; + Workspace: unknown; +}; + +/** + * Dynamically loads LWC dependencies based on version channel + */ +export class DependencyLoader { + private static loadedModules: Map = new Map(); + + /** + * Loads the LWC dev server module for the specified channel + * Uses dynamic import to load the aliased package at runtime + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded module + */ + public static async loadLwcDevServer(channel: VersionChannel): Promise { + // Check cache first + if (this.loadedModules.has(channel)) { + return this.loadedModules.get(channel)!; + } + + // Construct the aliased package name + const packageName = `@lwc/lwc-dev-server-${channel}`; + + try { + // Dynamic import of the aliased package + const module = (await import(packageName)) as LwcDevServerModule; + this.loadedModules.set(channel, module); + return module; + } catch (error) { + throw new Error( + `Failed to load LWC dev server for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Loads the LWC compiler module for the specified channel + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded compiler module + */ + public static async loadLwcCompiler(channel: VersionChannel): Promise { + const packageName = `@lwc/sfdc-lwc-compiler-${channel}`; + + try { + return (await import(packageName)) as unknown; + } catch (error) { + throw new Error( + `Failed to load LWC compiler for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Loads the base LWC module for the specified channel + * + * @param channel - The version channel ('latest' or 'prerelease') + * @returns The loaded LWC module + */ + public static async loadLwc(channel: VersionChannel): Promise { + const packageName = `lwc-${channel}`; + + try { + return (await import(packageName)) as unknown; + } catch (error) { + throw new Error( + `Failed to load LWC for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + /** + * Clears the module cache (useful for testing) + */ + public static clearCache(): void { + this.loadedModules.clear(); + } +} diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 44a14863..65bbf7a0 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -14,13 +14,8 @@ * limitations under the License. */ -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'); +import { Connection } from '@salesforce/core'; +import { VersionChannel, VersionResolver } from './versionResolver.js'; type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -36,36 +31,7 @@ export type AppDefinition = { /** * 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 @@ -162,51 +128,65 @@ export class OrgUtils { } /** - * 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. + * Determines the version channel for the connected org * - * @param connection the connection to the org + * @param connection - The connection to the org + * @param overrideChannel - Optional manual override from flag or env var + * @returns The version channel to use for dependencies + * @throws Error if the org version is not supported or invalid override provided */ - public static ensureMatchingAPIVersion(connection: Connection): void { - // Testing purposes only - using this flag may cause local development to not function correctly - if (process.env.SKIP_API_VERSION_CHECK === 'true') { - return; + public static getVersionChannel(connection: Connection, overrideChannel?: VersionChannel): VersionChannel { + // Priority 1: Explicit override parameter (from --version-channel flag) + if (overrideChannel) { + return overrideChannel; + } + + // Priority 2: Environment variable override + const envOverride = process.env.FORCE_VERSION_CHANNEL; + if (envOverride) { + const validChannels: VersionChannel[] = ['latest', 'prerelease']; + if (validChannels.includes(envOverride as VersionChannel)) { + return envOverride as VersionChannel; + } else { + throw new Error( + `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}` + ); + } } - const dirname = path.dirname(url.fileURLToPath(import.meta.url)); - const packageJsonFilePath = path.resolve(dirname, '../../package.json'); + // Priority 3: Skip check for testing (legacy compatibility) + if (process.env.SKIP_API_VERSION_CHECK === 'true') { + return VersionResolver.getDefaultChannel(); + } - const pkg = CommonUtils.loadJsonFromFile(packageJsonFilePath) as { - name: string; - apiVersionMetadata: ApiVersionMetadata; - }; - const targetVersion = pkg.apiVersionMetadata.target.versionNumber; + // Priority 4: Automatic detection based on org version const orgVersion = connection.version; - if (Version.same(orgVersion, targetVersion) === false) { - let errorMessage = messages.getMessage('error.org.api-mismatch.message', [orgVersion, targetVersion]); - // Find the tag (if any) that can support this org version - const tagName = pkg.apiVersionMetadata.versionToTagMappings.find( - (info) => info.versionNumber === orgVersion - )?.tagName; - if (tagName) { - const remediation = messages.getMessage('error.org.api-mismatch.remediation', [ - tagName, - `${pkg.name}@${tagName}`, - ]); - errorMessage = `${errorMessage} ${remediation}`; + try { + const orgId = connection.getAuthInfoFields().orgId; + if (!orgId) { + throw new Error('Could not determine org ID from connection.'); } - - // 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); + return VersionResolver.resolveChannelWithCache(orgId, orgVersion); + } catch (error) { + // Enhance error with helpful message + throw new Error( + `${error instanceof Error ? error.message : String(error)}\n` + + `Your org is on API version ${orgVersion}. ` + + 'Please ensure you are using the correct version of the CLI and this plugin.' + ); } } + + /** + * 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 + * @deprecated Use getVersionChannel instead + */ + public static ensureMatchingAPIVersion(connection: Connection): void { + this.getVersionChannel(connection); + } } diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index a075b35a..6c58ad36 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -57,7 +57,8 @@ export class PreviewUtils { ports: { httpPort: number; httpsPort: number }, logger?: Logger ): string { - return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger as any); } /** @@ -109,8 +110,10 @@ export class PreviewUtils { device = platform === Platform.ios - ? await new AppleDeviceManager(logger).getDevice(deviceId) - : await new AndroidDeviceManager(logger).getDevice(deviceId); + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + await new AppleDeviceManager(logger as any).getDevice(deviceId) + : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + await new AndroidDeviceManager(logger as any).getDevice(deviceId); } else { logger?.debug('Prompting the user to select a device.'); @@ -438,7 +441,7 @@ export class PreviewUtils { return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled'))); } - OrgUtils.ensureMatchingAPIVersion(connection); + OrgUtils.getVersionChannel(connection); const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection); const ldpServerToken = appServerIdentity.identityToken; diff --git a/src/shared/promptUtils.ts b/src/shared/promptUtils.ts index c60784c8..34999e37 100644 --- a/src/shared/promptUtils.ts +++ b/src/shared/promptUtils.ts @@ -79,8 +79,10 @@ export class PromptUtils { ): Promise { const availableDevices = platform === Platform.ios - ? await new AppleDeviceManager(logger).enumerateDevices() - : await new AndroidDeviceManager(logger).enumerateDevices(); + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + await new AppleDeviceManager(logger as any).enumerateDevices() + : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + await new AndroidDeviceManager(logger as any).enumerateDevices(); if (!availableDevices || availableDevices.length === 0) { throw new Error(messages.getMessage('error.device.enumeration')); diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts new file mode 100644 index 00000000..4a48f291 --- /dev/null +++ b/src/shared/versionResolver.ts @@ -0,0 +1,183 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from 'node:path'; +import url from 'node:url'; +import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; + +/** + * Resolves org API version to appropriate dependency channel + */ +export type VersionChannel = 'latest' | 'prerelease' | 'next'; + +export type ChannelConfig = { + supportedApiVersions: string[]; + dependencies: { + [key: string]: string; + }; +}; + +type CacheEntry = { + apiVersion: string; + channel: VersionChannel; + timestamp: number; +}; + +type PackageJson = { + apiVersionMetadata: { + channels: { + [key in VersionChannel]: ChannelConfig; + }; + defaultChannel: string; + }; +}; + +export class VersionResolver { + private static channelMetadata: Map | null = null; + private static versionCache: Map = new Map(); + private static readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + + /** + * Given an org API version, returns the appropriate channel + * + * @param orgApiVersion - The API version from the org (e.g., "65.0") + * @returns The channel to use ('latest' or 'prerelease') + * @throws Error if the API version is not supported by any channel + */ + public static resolveChannel(orgApiVersion: string): VersionChannel { + const channels = this.loadChannelMetadata(); + + for (const [channel, config] of channels.entries()) { + if (config.supportedApiVersions.includes(orgApiVersion)) { + return channel; + } + } + + // If no exact match, try to find by major.minor comparison + const orgMajorMinor = this.getMajorMinor(orgApiVersion); + for (const [channel, config] of channels.entries()) { + for (const supportedVersion of config.supportedApiVersions) { + if (this.getMajorMinor(supportedVersion) === orgMajorMinor) { + return channel; + } + } + } + + throw new Error( + `Unsupported org API version: ${orgApiVersion}. This plugin supports: ${this.getSupportedVersionsList()}` + ); + } + + /** + * Resolves channel with caching support + * + * @param orgId - Unique identifier for the org + * @param orgApiVersion - The API version from the org + * @returns The channel to use + */ + public static resolveChannelWithCache(orgId: string, orgApiVersion: string): VersionChannel { + // Check cache first + const cached = this.versionCache.get(orgId); + if (cached) { + const age = Date.now() - cached.timestamp; + if (age < this.CACHE_TTL_MS && cached.apiVersion === orgApiVersion) { + return cached.channel; + } + // Cache expired or version changed, remove it + this.versionCache.delete(orgId); + } + + // Resolve and cache + const channel = this.resolveChannel(orgApiVersion); + this.versionCache.set(orgId, { + apiVersion: orgApiVersion, + channel, + timestamp: Date.now(), + }); + + return channel; + } + + /** + * Returns the default channel from package.json + */ + public static getDefaultChannel(): VersionChannel { + const packageJson = this.getPackageJson(); + return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; + } + + /** + * Clears the version cache (useful for testing or when orgs are upgraded) + */ + public static clearCache(): void { + this.versionCache.clear(); + this.channelMetadata = null; + } + + /** + * Removes a specific org from the cache + */ + public static removeCacheEntry(orgId: string): void { + this.versionCache.delete(orgId); + } + + /** + * Loads channel metadata from package.json + */ + private static loadChannelMetadata(): Map { + if (this.channelMetadata) { + return this.channelMetadata; + } + + const packageJson = this.getPackageJson(); + const channels = packageJson.apiVersionMetadata.channels; + + this.channelMetadata = new Map(); + for (const [channel, config] of Object.entries(channels)) { + this.channelMetadata.set(channel as VersionChannel, config); + } + + return this.channelMetadata; + } + + /** + * Extracts major.minor from a version string (e.g., "65.0" from "65.0.1") + */ + private static getMajorMinor(version: string): string { + const parts = version.split('.'); + return `${parts[0]}.${parts[1]}`; + } + + /** + * Returns a formatted list of all supported API versions + */ + private static getSupportedVersionsList(): string { + const channels = this.loadChannelMetadata(); + const allVersions: string[] = []; + + for (const config of channels.values()) { + allVersions.push(...config.supportedApiVersions); + } + + return allVersions.join(', '); + } + + private static getPackageJson(): PackageJson { + const dirname = path.dirname(url.fileURLToPath(import.meta.url)); + const packageJsonFilePath = path.resolve(dirname, '../../package.json'); + return CommonUtils.loadJsonFromFile(packageJsonFilePath) as unknown as PackageJson; + } +} diff --git a/src/types/aliased-deps.d.ts b/src/types/aliased-deps.d.ts new file mode 100644 index 00000000..b974b074 --- /dev/null +++ b/src/types/aliased-deps.d.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module '@lwc/lwc-dev-server-latest' { + export * from '@lwc/lwc-dev-server'; +} + +declare module '@lwc/lwc-dev-server-prerelease' { + export * from '@lwc/lwc-dev-server'; +} + +declare module '@lwc/lwc-dev-server-next' { + export * from '@lwc/lwc-dev-server'; +} + +declare module '@lwc/sfdc-lwc-compiler-latest' { + export * from '@lwc/sfdc-lwc-compiler'; +} + +declare module '@lwc/sfdc-lwc-compiler-prerelease' { + export * from '@lwc/sfdc-lwc-compiler'; +} + +declare module '@lwc/sfdc-lwc-compiler-next' { + export * from '@lwc/sfdc-lwc-compiler'; +} + +declare module 'lwc-latest' { + export * from 'lwc'; +} + +declare module 'lwc-prerelease' { + export * from 'lwc'; +} + +declare module 'lwc-next' { + export * from 'lwc'; +} diff --git a/test/commands/lightning/dev/app.test.ts b/test/commands/lightning/dev/app.test.ts index 27dc2a77..8dfac728 100644 --- a/test/commands/lightning/dev/app.test.ts +++ b/test/commands/lightning/dev/app.test.ts @@ -90,6 +90,7 @@ describe('lightning dev app', () => { testIdentityData.usernameToServerEntityIdMap[testUsername] = testLdpServerId; beforeEach(async () => { + process.env.SKIP_API_VERSION_CHECK = 'true'; stubUx($$.SANDBOX); stubSpinner($$.SANDBOX); await $$.stubAuths(testOrgData); @@ -112,6 +113,7 @@ describe('lightning dev app', () => { }); afterEach(() => { + delete process.env.SKIP_API_VERSION_CHECK; $$.restore(); }); diff --git a/test/shared/dependencyLoader.test.ts b/test/shared/dependencyLoader.test.ts new file mode 100644 index 00000000..6c0689d9 --- /dev/null +++ b/test/shared/dependencyLoader.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { DependencyLoader } from '../../src/shared/dependencyLoader.js'; + +describe('DependencyLoader', () => { + beforeEach(() => { + DependencyLoader.clearCache(); + }); + + it('exists and has expected methods', () => { + expect(typeof DependencyLoader.loadLwcDevServer).to.equal('function'); + expect(typeof DependencyLoader.loadLwcCompiler).to.equal('function'); + expect(typeof DependencyLoader.loadLwc).to.equal('function'); + expect(typeof DependencyLoader.clearCache).to.equal('function'); + }); + + it('loads the aliased package (real import call)', async () => { + // This will actually try to call import() which should work since we ran yarn install. + // However, loading LWC modules in Node might still trigger ReferenceErrors if browser globals are missing. + // We use a try-catch to handle both cases and just verify the attempt was made. + try { + const module = await DependencyLoader.loadLwcDevServer('latest'); + expect(module).to.exist; + } catch (error) { + // If it fails with a ReferenceError or similar, it's still "working" in terms of + // attempting to load the right package name. + const errorMessage = (error as Error).message; + if (errorMessage.includes('could not be imported')) { + expect(errorMessage).to.include('@lwc/lwc-dev-server-latest'); + } else { + // Other errors (like ReferenceError: Element is not defined) mean the package WAS found and loaded + expect(errorMessage).to.not.include('could not be imported'); + } + } + }); +}); diff --git a/test/shared/orgUtils.test.ts b/test/shared/orgUtils.test.ts index e08d93f8..23d0946b 100644 --- a/test/shared/orgUtils.test.ts +++ b/test/shared/orgUtils.test.ts @@ -17,15 +17,86 @@ import { TestContext } from '@salesforce/core/testSetup'; import { expect } from 'chai'; import { AuthInfo, Connection } from '@salesforce/core'; +import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import { OrgUtils } from '../../src/shared/orgUtils.js'; +import { VersionResolver } from '../../src/shared/versionResolver.js'; describe('orgUtils', () => { const $$ = new TestContext(); + const mockPackageJson = { + apiVersionMetadata: { + channels: { + latest: { + supportedApiVersions: ['65.0'], + dependencies: {}, + }, + prerelease: { + supportedApiVersions: ['66.0'], + dependencies: {}, + }, + }, + defaultChannel: 'latest', + }, + }; + + beforeEach(() => { + $$.SANDBOX.stub(CommonUtils, 'loadJsonFromFile').returns(mockPackageJson); + VersionResolver.clearCache(); + }); + afterEach(() => { $$.restore(); }); + describe('getVersionChannel', () => { + it('returns override channel if provided', async () => { + const conn = new Connection({ authInfo: new AuthInfo() }); + const channel = OrgUtils.getVersionChannel(conn, 'prerelease'); + expect(channel).to.equal('prerelease'); + }); + + it('returns channel from FORCE_VERSION_CHANNEL env var', async () => { + process.env.FORCE_VERSION_CHANNEL = 'prerelease'; + const conn = new Connection({ authInfo: new AuthInfo() }); + const channel = OrgUtils.getVersionChannel(conn); + expect(channel).to.equal('prerelease'); + delete process.env.FORCE_VERSION_CHANNEL; + }); + + it('throws error for invalid FORCE_VERSION_CHANNEL', async () => { + process.env.FORCE_VERSION_CHANNEL = 'invalid'; + const conn = new Connection({ authInfo: new AuthInfo() }); + expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Invalid FORCE_VERSION_CHANNEL/); + delete process.env.FORCE_VERSION_CHANNEL; + }); + + it('returns default channel when SKIP_API_VERSION_CHECK is true', async () => { + process.env.SKIP_API_VERSION_CHECK = 'true'; + const conn = new Connection({ authInfo: new AuthInfo() }); + const channel = OrgUtils.getVersionChannel(conn); + expect(channel).to.equal('latest'); + delete process.env.SKIP_API_VERSION_CHECK; + }); + + it('auto-detects channel based on org version', async () => { + const conn = new Connection({ authInfo: new AuthInfo() }); + $$.SANDBOX.stub(conn, 'version').get(() => '65.0'); + $$.SANDBOX.stub(conn, 'getAuthInfoFields').returns({ orgId: 'org1' }); + + const channel = OrgUtils.getVersionChannel(conn); + expect(channel).to.equal('latest'); + }); + + it('throws error for unsupported org version', async () => { + const conn = new Connection({ authInfo: new AuthInfo() }); + $$.SANDBOX.stub(conn, 'version').get(() => '64.0'); + $$.SANDBOX.stub(conn, 'getAuthInfoFields').returns({ orgId: 'org1' }); + + expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Unsupported org API version: 64.0/); + }); + }); + it('getAppDefinitionDurableId returns undefined when no matches found', async () => { $$.SANDBOX.stub(Connection.prototype, 'query').resolves({ records: [], done: true, totalSize: 0 }); const appId = await OrgUtils.getAppDefinitionDurableId(new Connection({ authInfo: new AuthInfo() }), 'blah'); diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index bfcec0a3..ca365428 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -31,7 +31,7 @@ import { SSLCertificateData, Version, } from '@salesforce/lwc-dev-mobile-core'; -import { AuthInfo, Connection, Logger, Org } from '@salesforce/core'; +import { AuthInfo, Connection, Org } from '@salesforce/core'; import { PreviewUtils as LwcDevMobileCorePreviewUtils } from '@salesforce/lwc-dev-mobile-core'; import { ConfigUtils, @@ -70,7 +70,12 @@ describe('previewUtils', () => { }; testIdentityData.usernameToServerEntityIdMap[testUsername] = testLdpServerId; + beforeEach(() => { + process.env.SKIP_API_VERSION_CHECK = 'true'; + }); + afterEach(() => { + delete process.env.SKIP_API_VERSION_CHECK; $$.restore(); }); @@ -322,10 +327,12 @@ describe('previewUtils', () => { 'generateWebSocketUrlForLocalDevServer' ).returns(mockUrl); - const result = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, {} as Logger); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + const result = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, {} as any); expect(result).to.equal(mockUrl); - expect(generateWebSocketUrlStub.calledOnceWith(platform, ports, {} as Logger)).to.be.true; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + expect(generateWebSocketUrlStub.calledOnceWith(platform, ports, {} as any)).to.be.true; }); it('initializePreviewConnection succeeds with valid org', async () => { diff --git a/test/shared/versionResolver.test.ts b/test/shared/versionResolver.test.ts new file mode 100644 index 00000000..e16c7c57 --- /dev/null +++ b/test/shared/versionResolver.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestContext } from '@salesforce/core/testSetup'; +import { expect } from 'chai'; +import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; +import { VersionResolver } from '../../src/shared/versionResolver.js'; + +describe('VersionResolver', () => { + const $$ = new TestContext(); + + const mockPackageJson = { + apiVersionMetadata: { + channels: { + latest: { + supportedApiVersions: ['65.0'], + dependencies: { + '@lwc/lwc-dev-server': '~13.2.x', + lwc: '~8.23.x', + }, + }, + prerelease: { + supportedApiVersions: ['66.0'], + dependencies: { + '@lwc/lwc-dev-server': '~13.3.x', + lwc: '~8.24.x', + }, + }, + next: { + supportedApiVersions: ['67.0'], + dependencies: { + '@lwc/lwc-dev-server': '~13.3.x', + lwc: '~8.24.x', + }, + }, + }, + defaultChannel: 'latest', + }, + }; + + beforeEach(() => { + $$.SANDBOX.stub(CommonUtils, 'loadJsonFromFile').returns(mockPackageJson); + VersionResolver.clearCache(); + }); + + afterEach(() => { + $$.restore(); + }); + + it('resolveChannel returns correct channel for exact match', () => { + expect(VersionResolver.resolveChannel('65.0')).to.equal('latest'); + expect(VersionResolver.resolveChannel('66.0')).to.equal('prerelease'); + expect(VersionResolver.resolveChannel('67.0')).to.equal('next'); + }); + + it('resolveChannel returns correct channel for major.minor match', () => { + expect(VersionResolver.resolveChannel('65.0.1')).to.equal('latest'); + expect(VersionResolver.resolveChannel('66.0.5')).to.equal('prerelease'); + expect(VersionResolver.resolveChannel('67.0.2')).to.equal('next'); + }); + + it('resolveChannel throws error for unsupported version', () => { + expect(() => VersionResolver.resolveChannel('64.0')).to.throw(/Unsupported org API version: 64.0/); + }); + + it('resolveChannelWithCache returns cached value', () => { + const resolveSpy = $$.SANDBOX.spy(VersionResolver, 'resolveChannel'); + + // First call - resolves + const channel1 = VersionResolver.resolveChannelWithCache('org1', '65.0'); + expect(channel1).to.equal('latest'); + expect(resolveSpy.calledOnce).to.be.true; + + // Second call - cached + const channel2 = VersionResolver.resolveChannelWithCache('org1', '65.0'); + expect(channel2).to.equal('latest'); + expect(resolveSpy.calledOnce).to.be.true; + + // Different org - resolves + const channel3 = VersionResolver.resolveChannelWithCache('org2', '65.0'); + expect(channel3).to.equal('latest'); + expect(resolveSpy.calledTwice).to.be.true; + }); + + it('resolveChannelWithCache invalidates cache when version changes', () => { + VersionResolver.resolveChannelWithCache('org1', '65.0'); + + const resolveSpy = $$.SANDBOX.spy(VersionResolver, 'resolveChannel'); + + // Version changed - re-resolves + const channel = VersionResolver.resolveChannelWithCache('org1', '66.0'); + expect(channel).to.equal('prerelease'); + expect(resolveSpy.calledOnce).to.be.true; + }); + + it('getDefaultChannel returns default from package.json', () => { + expect(VersionResolver.getDefaultChannel()).to.equal('latest'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 39add557..7b63b48a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -677,6 +677,27 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.0.tgz#9fc6fd58c2a6a15243cd13983224968392070790" integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw== +"@babel/core@7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496" + integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/core@7.28.5", "@babel/core@^7.23.9", "@babel/core@^7.26.10", "@babel/core@^7.9.0": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.5.tgz#4c81b35e51e1b734f510c99b07dfbc7bbbb48f7e" @@ -749,6 +770,17 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" +"@babel/generator@^7.28.3": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" + integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== + dependencies: + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/generator@^7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.5.tgz#712722d5e50f44d07bc7ac9fe84438742dd61298" @@ -932,6 +964,13 @@ dependencies: "@babel/types" "^7.28.5" +"@babel/parser@^7.28.4", "@babel/parser@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" + integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== + dependencies: + "@babel/types" "^7.28.6" + "@babel/plugin-syntax-decorators@7.27.1": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz#ee7dd9590aeebc05f9d4c8c0560007b05979a63d" @@ -1057,6 +1096,14 @@ "@babel/types" "^7.28.5" debug "^4.3.1" +"@babel/types@7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.4.tgz#0a4e618f4c60a7cd6c11cb2d48060e4dbe38ac3a" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/types@7.28.5", "@babel/types@^7.22.10", "@babel/types@^7.26.10", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.9.0", "@babel/types@~7.28.5": version "7.28.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" @@ -1065,6 +1112,14 @@ "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" +"@babel/types@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" + integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@commitlint/cli@^17.1.2": version "17.8.1" resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-17.8.1.tgz#10492114a022c91dcfb1d84dac773abb3db76d33" @@ -1513,13 +1568,20 @@ debug "^4.3.1" minimatch "^3.1.2" -"@eslint/config-helpers@^0.4.2": +"@eslint/config-helpers@^0.4.1", "@eslint/config-helpers@^0.4.2": version "0.4.2" resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda" integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== dependencies: "@eslint/core" "^0.17.0" +"@eslint/core@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.16.0.tgz#490254f275ba9667ddbab344f4f0a6b7a7bd7209" + integrity sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q== + dependencies: + "@types/json-schema" "^7.0.15" + "@eslint/core@^0.17.0": version "0.17.0" resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c" @@ -1562,6 +1624,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== +"@eslint/js@9.38.0": + version "9.38.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.38.0.tgz#f7aa9c7577577f53302c1d795643589d7709ebd1" + integrity sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A== + "@eslint/js@9.39.2": version "9.39.2" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" @@ -1577,7 +1644,7 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad" integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== -"@eslint/plugin-kit@^0.4.1": +"@eslint/plugin-kit@^0.4.0", "@eslint/plugin-kit@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2" integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== @@ -2210,11 +2277,41 @@ dependencies: "@locker/shared" "0.24.6" +"@lwc/aria-reflection@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/aria-reflection/-/aria-reflection-8.23.0.tgz#b4d1f31e5f380d49e28b6dfe07c505d0657024e4" + integrity sha512-WGaMjNc84art9cYtkMf15dCKZ7M1x1yldsbqctiKOTl6mN6d7EDRaV+046EkFMkC/FwQbspWTHF9x1UGqWV8pw== + +"@lwc/aria-reflection@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/aria-reflection/-/aria-reflection-8.24.0.tgz#d8b0fd092fd6ea92d5d42a5d45e004b98ec6372b" + integrity sha512-X6wYl0omMVsElJTpi+8ntyCaOfoRjtVF27K3SnaGaNkntIby/Suz/XKJN1KKgD+hAyQtWzxEsrAbdCYani2ROQ== + "@lwc/aria-reflection@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/aria-reflection/-/aria-reflection-8.27.0.tgz#7fa9cc6652c67ed73fdaf2a2a4f29dca2c9160c2" integrity sha512-sOLY5Mm8lWscJU+aDg0/v7sX2fphwO2ZSL2b54+9gvRVgkC+2X4MboPQwTWTkCecJYgeOf1HatXhqMorkwQftg== +"@lwc/babel-plugin-component@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/babel-plugin-component/-/babel-plugin-component-8.23.0.tgz#756762acb6c5ba17f18614d8a858f377a4e55fbc" + integrity sha512-Dct16w1mSoL0gIZFNSQI6EQjOAnOkmdbCBAf2PMD7mJXQKNYYgb4RcA4BDIoZZJwe5nH7rd6q47YaeD7pdxCvg== + dependencies: + "@babel/helper-module-imports" "7.27.1" + "@lwc/errors" "8.23.0" + "@lwc/shared" "8.23.0" + line-column "~1.0.2" + +"@lwc/babel-plugin-component@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/babel-plugin-component/-/babel-plugin-component-8.24.0.tgz#89aae9ce4dfab40bde1d1d01b087a48581d42de2" + integrity sha512-WaJjoLzNqrhVUHudxv+O04OU5GRNVDfYb1s6fKDYV2VdCHRyl6LQ4v2zZmoeMbahg/S0AO4fyGWxAZS2D7Zuyw== + dependencies: + "@babel/helper-module-imports" "7.27.1" + "@lwc/errors" "8.24.0" + "@lwc/shared" "8.24.0" + line-column "~1.0.2" + "@lwc/babel-plugin-component@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/babel-plugin-component/-/babel-plugin-component-8.27.0.tgz#fc5f117b2616076169a8db5565b520dd0a61a96a" @@ -2225,6 +2322,42 @@ "@lwc/shared" "8.27.0" line-column "~1.0.2" +"@lwc/compiler@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/compiler/-/compiler-8.23.0.tgz#bc5f5406e8d71d4675cfe7276b2443544116f0a7" + integrity sha512-ejGsAR9c+Lv3Xtu6XC8RbNl7XUIt3tIES3g4r3FanrU5FHQ2fsqXdLTT2I9jxlTiKHiRnvpq49pQQLWQcUPr0Q== + dependencies: + "@babel/core" "7.28.4" + "@babel/plugin-transform-async-generator-functions" "7.28.0" + "@babel/plugin-transform-async-to-generator" "7.27.1" + "@babel/plugin-transform-class-properties" "7.27.1" + "@babel/plugin-transform-object-rest-spread" "7.28.4" + "@locker/babel-plugin-transform-unforgeables" "0.22.0" + "@lwc/babel-plugin-component" "8.23.0" + "@lwc/errors" "8.23.0" + "@lwc/shared" "8.23.0" + "@lwc/ssr-compiler" "8.23.0" + "@lwc/style-compiler" "8.23.0" + "@lwc/template-compiler" "8.23.0" + +"@lwc/compiler@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/compiler/-/compiler-8.24.0.tgz#a9227bbd9c5bcd81764fd2895b23750febf69c7d" + integrity sha512-4WlQx1b7V0HlwQY3dLJ7XNdlLPRP8AQlNsfkgkbHGi1YtLx7acFr2ttGHBimsHc4uBHdqdguQ3IQD54agfj+CQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/plugin-transform-async-generator-functions" "7.28.0" + "@babel/plugin-transform-async-to-generator" "7.27.1" + "@babel/plugin-transform-class-properties" "7.27.1" + "@babel/plugin-transform-object-rest-spread" "7.28.4" + "@locker/babel-plugin-transform-unforgeables" "0.22.0" + "@lwc/babel-plugin-component" "8.24.0" + "@lwc/errors" "8.24.0" + "@lwc/shared" "8.24.0" + "@lwc/ssr-compiler" "8.24.0" + "@lwc/style-compiler" "8.24.0" + "@lwc/template-compiler" "8.24.0" + "@lwc/compiler@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/compiler/-/compiler-8.27.0.tgz#6ec761d07d5cb369f223f341ee11e5552928d153" @@ -2243,6 +2376,13 @@ "@lwc/style-compiler" "8.27.0" "@lwc/template-compiler" "8.27.0" +"@lwc/dev-server-plugin-lex@13.2.20": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/dev-server-plugin-lex/-/dev-server-plugin-lex-13.2.20.tgz#f48e0d43f42e4316f18eb1ed26fcbfb5cff1749a" + integrity sha512-+VvN5sLILmT2KIWRolx8S9aGFV3mPqn2u78Q0g7UAXVqT2ld/4ZSfEUDVXSrTGNu+4MZbBxU31WZzeuxc8vkmA== + dependencies: + magic-string "~0.30.21" + "@lwc/dev-server-plugin-lex@13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/dev-server-plugin-lex/-/dev-server-plugin-lex-13.3.8.tgz#9a05785c11b4a5196e2f9a2eaeeb9bd707799d2a" @@ -2250,6 +2390,31 @@ dependencies: magic-string "~0.30.21" +"@lwc/dev-server-plugin-lex@13.3.9": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/dev-server-plugin-lex/-/dev-server-plugin-lex-13.3.9.tgz#cef2da9034b55c702b1d43d749e0b47c1299167e" + integrity sha512-FAswqYnV+ypLIWmAdpnut9UMneZtSPUQ0SMEO2+A/lqit9HAMMnKObEk7pgX2rZcNRKYeddT7hj3QuGcWeYOoA== + dependencies: + magic-string "~0.30.21" + +"@lwc/engine-core@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-core/-/engine-core-8.23.0.tgz#04ac9204140685c5d4f69cc3fe8c8bb6d7ffe4df" + integrity sha512-YFobCuGRv0me1FLhJqtSDm/WxTpRyPtdHbfBe/6AKWDFfYYPk83tcMFTTS7ndE3LrYno2aP9ABHdg5BtAl55iA== + dependencies: + "@lwc/features" "8.23.0" + "@lwc/shared" "8.23.0" + "@lwc/signals" "8.23.0" + +"@lwc/engine-core@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-core/-/engine-core-8.24.0.tgz#38c5575a7e8e48048f0d77664b667ce9f3111bff" + integrity sha512-XYMx7wmVY84g7mWzEUUo/26gN3x6/H91Naaul5jKbFJXHrf1YQTF794S+5YPI82GlZYFiDNMYKqi3FXAqvyUpQ== + dependencies: + "@lwc/features" "8.24.0" + "@lwc/shared" "8.24.0" + "@lwc/signals" "8.24.0" + "@lwc/engine-core@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/engine-core/-/engine-core-8.27.0.tgz#78ed2748c045339d51ac4a1d3ca656346f9aea2e" @@ -2259,16 +2424,46 @@ "@lwc/shared" "8.27.0" "@lwc/signals" "8.27.0" +"@lwc/engine-dom@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-dom/-/engine-dom-8.23.0.tgz#dd45d7c33a318dcdf600796478a7e03e759122df" + integrity sha512-cbMVwFkYmhFFmCpnSYBePk8wnpdhqESVFDZJqFgeR4Tb0LBVlI18gGqHn9MmjxT3SnurDp1EW/ArH8QFYJgf1g== + +"@lwc/engine-dom@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-dom/-/engine-dom-8.24.0.tgz#46985f32c9f9c0522c209b42e72d3b435c1ed7f8" + integrity sha512-H0MeTt1DEYKY98m2ZblpSazuhZzKxxChi/iM/4n4fFWtBF87AAoQtGt1fFnEPCq9D9lGmVF28YQdBjL7NNw6nQ== + "@lwc/engine-dom@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/engine-dom/-/engine-dom-8.27.0.tgz#5be044552038c84811d950c1f483115c4cdfc0b4" integrity sha512-npXl2hx6gyc7wum9wLPCzHz9J9iChiBJk1dKG3J/oY/EJiWxhaJxk0pCIhscn8/7n1wSeQ8DY4B8KeoyFpvmNw== +"@lwc/engine-server@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-server/-/engine-server-8.23.0.tgz#6675516060b8886c23e18faf92ab31b69d6c7053" + integrity sha512-h4HOYAoHWAPEwITroai8yAy6YSlqMXRLdVZNRNH/ZEXkz5Hom+h16BbnGGeGCNqZgGrm58LnCPYmmzeXIZ1aPQ== + +"@lwc/engine-server@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/engine-server/-/engine-server-8.24.0.tgz#bbf00cbc822f44d0dae605d69ea77cd09123527d" + integrity sha512-FTgKGYj1pXtiUgXyi+m5BQCe4IMVAp8eF2sRY+NGV0INrmpKfVQaFd5aakGNl27EGEnr/kWUZrY+/dbtwErIdQ== + "@lwc/engine-server@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/engine-server/-/engine-server-8.27.0.tgz#124fc02d8259f1c6b91322463926bbc51f8efdbf" integrity sha512-ArzdTenZ3MlK5fovAmQw4zJ2d4UwTC+DyFQ0lG453pj57X5KcUAjPOD/dtFoZmPqUU7UC5y85mDIelBWdryA9g== +"@lwc/errors@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/errors/-/errors-8.23.0.tgz#7c230010e197062a0eae04a4caa4abe4e7df4af3" + integrity sha512-cqziHl/aDg0pgAIm9HhNz2OrR0t08vML7+FNrqgrnaSwLgYCYp2WPNmvMjD5ZRy/bIBESwA/dCWf9Cs7Vo/ZGw== + +"@lwc/errors@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/errors/-/errors-8.24.0.tgz#348eff6a6022604273d397b624ca96800b242569" + integrity sha512-shYEI9wjVXjKNRXAMAXHMZ23jvMuJIIkctwpkzFx09PcueflwXu9NfNsUlgK8662cOvnvlpAuYvcT+G0k1/wrw== + "@lwc/errors@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/errors/-/errors-8.27.0.tgz#ffb560bfd48953d553b71a9e573ceefe7d029bfe" @@ -2290,6 +2485,20 @@ globals "~15.14.0" minimatch "~9.0.4" +"@lwc/features@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/features/-/features-8.23.0.tgz#7e0578c89dc39e62d50b732facca7e9e969539e9" + integrity sha512-Y1yVOH6LAJMWJaeUHJwjx7OHjbRxycdSSZKewhQlRTGLrmca1Rtpi31nfrRtcCdOkxfF3oPMFlPlA4ZKDyA6aA== + dependencies: + "@lwc/shared" "8.23.0" + +"@lwc/features@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/features/-/features-8.24.0.tgz#524ee89f11543996b3117e54b62fde867bbf8c44" + integrity sha512-h9FX63i6/wFUzU48jgrRxH3cmxdSZ/HRPwoR3jKb1qLKL0kKkWPF3EuRnXRgqhcD2qhFteTSAUgtwBcRFzvnSg== + dependencies: + "@lwc/shared" "8.24.0" + "@lwc/features@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/features/-/features-8.27.0.tgz#7d2a9af45c4bb4e62cc89ec102c9cd115283e881" @@ -2297,11 +2506,49 @@ dependencies: "@lwc/shared" "8.27.0" +"@lwc/lwc-dev-server-latest@npm:@lwc/lwc-dev-server@~13.2.x": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server/-/lwc-dev-server-13.2.20.tgz#fef34e855aa947a76f4f80365be9b7e2a930229b" + integrity sha512-7I3u3pINBWFvhejEjvxhLGmaGko0y4Mw9ean0mf4F9d90jX+t4SIN9rtN2In9Fi+uhmZAG66/QqqldKVul8PFQ== + dependencies: + "@lwc/lwc-dev-server-types" "13.2.20" + "@lwc/sfdc-lwc-compiler" "13.2.20" + chalk "~5.6.2" + chokidar "~3.6.0" + commander "~14.0.2" + fast-xml-parser "^5.3.0" + glob "^11.0.3" + ws "^8.18.3" + +"@lwc/lwc-dev-server-prerelease@npm:@lwc/lwc-dev-server@~13.3.x": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server/-/lwc-dev-server-13.3.9.tgz#4e922c079738d9b0a3cba64e301a357bc479ca45" + integrity sha512-4HfMSacB0wXnn8tXjb2SIokswjEx3z5bdGQvqCuycF6RPtNel+KZE/4gBtjZtuwRHMXTO8GeyoPmFPTWLi8aVQ== + dependencies: + "@lwc/lwc-dev-server-types" "13.3.9" + "@lwc/sfdc-lwc-compiler" "13.3.9" + chalk "~5.6.2" + chokidar "~3.6.0" + commander "~14.0.2" + fast-xml-parser "^5.3.0" + glob "^11.0.3" + ws "^8.18.3" + +"@lwc/lwc-dev-server-types@13.2.20": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server-types/-/lwc-dev-server-types-13.2.20.tgz#aedf477c6b451db452c109729e13de6662fb1585" + integrity sha512-6nMFOoNvusOEjFyQ1YoXzKTGJsTdj/IcneJ3W0ac4B4xxz2twcZybeG4jXZhy970sjdWcaWpqp3Vu5kGtJsnJA== + "@lwc/lwc-dev-server-types@13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server-types/-/lwc-dev-server-types-13.3.8.tgz#43e54f846933193b7a407cbe0b78807273232c48" integrity sha512-ipcr+9KGJYa9D8F/qM817+89wH/x9x6zGDjq6o4jIvO49DFULBNXuSAxC+0zbYTALtdcsnM4YrAufciK0nwf5w== +"@lwc/lwc-dev-server-types@13.3.9": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server-types/-/lwc-dev-server-types-13.3.9.tgz#7f392f845552701650f60cbcc0b4893acfba0678" + integrity sha512-0FUyfDrxtvf0IdiH+ql5j78DwRiIuk5M2QJ+jIFx1OuQrProBp3s+YKLJoYffkmS3rcEYgg/+LbbtA5dba1ICQ== + "@lwc/lwc-dev-server@~13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server/-/lwc-dev-server-13.3.8.tgz#2e8215c6c91eabbc3abe08e107daa8edcdb1ba73" @@ -2316,6 +2563,19 @@ glob "^11.0.3" ws "^8.18.3" +"@lwc/metadata@13.2.20": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/metadata/-/metadata-13.2.20.tgz#ff04e6ad34eb8d6f93178d57cc5e1df6a9eb7572" + integrity sha512-Y/SfQTseaO+EqHMFV9yBtaxyUYnAA8FRHrQBfJ/y8oeS7jRr+DSyiOFHJpx5NqsyAtmx2KTjggmWow6GgfjD+A== + dependencies: + "@babel/parser" "~7.28.5" + "@babel/traverse" "~7.28.5" + "@babel/types" "~7.28.5" + "@lwc/sfdc-compiler-utils" "13.2.20" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + postcss-value-parser "~4.2.0" + "@lwc/metadata@13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/metadata/-/metadata-13.3.8.tgz#d8a126cc7b770b27c1b280dc85030d12cf78fd8d" @@ -2329,6 +2589,33 @@ postcss-selector-parser "~7.1.0" postcss-value-parser "~4.2.0" +"@lwc/metadata@13.3.9": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/metadata/-/metadata-13.3.9.tgz#3228facd12b0cf48b8ce59b7a2e883836b66453d" + integrity sha512-otjfamwU6zyYs/DXwyaLhavQ/PAtZ7ot3EU0ZUpcBuPgp9HKdR3aVCigfVNTh4wdYosru1uZbAkLQbHJo/15KA== + dependencies: + "@babel/parser" "~7.28.5" + "@babel/traverse" "~7.28.5" + "@babel/types" "~7.28.5" + "@lwc/sfdc-compiler-utils" "13.3.9" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + postcss-value-parser "~4.2.0" + +"@lwc/module-resolver@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/module-resolver/-/module-resolver-8.23.0.tgz#f98581796558d484516097b7b04121453846f9d1" + integrity sha512-ZqZ/402NvVswMK2HMhwH6Fmkzn19xn5Yx7VZr1QmIefKXr8OKqFSlsySujN3CSwNH9XHybyREWe4TXlkT7LHFw== + dependencies: + resolve "~1.22.10" + +"@lwc/module-resolver@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/module-resolver/-/module-resolver-8.24.0.tgz#fca4582d31af6f3f62638d84de292f0e18aaa1ce" + integrity sha512-Mqj5PCU46coyUMWZ527JC6EwEv0DdIxEJpSK5RgLjgCGdSK7NXYx2zvTnIbWiLLpcj9u82DvFs/FFJu7ixTHbA== + dependencies: + resolve "~1.22.11" + "@lwc/module-resolver@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/module-resolver/-/module-resolver-8.27.0.tgz#57cca88798846054a51a5fe872289ff4ae5fae98" @@ -2336,6 +2623,26 @@ dependencies: resolve "~1.22.11" +"@lwc/rollup-plugin@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/rollup-plugin/-/rollup-plugin-8.23.0.tgz#6821076f721f4b298b2c40d81832ffb55a37c9f6" + integrity sha512-bDlnRXWOVN4VE+/h1dj2KXuej9bED2A07CtxHPepCH4iIwpN6w+s/495zDndJgO/VppnZ3ZUiUooUrcDOrOmBA== + dependencies: + "@lwc/compiler" "8.23.0" + "@lwc/module-resolver" "8.23.0" + "@lwc/shared" "8.23.0" + "@rollup/pluginutils" "~5.3.0" + +"@lwc/rollup-plugin@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/rollup-plugin/-/rollup-plugin-8.24.0.tgz#5fb194a58563fd4f4a02d2c82c6ad56d3fe0612d" + integrity sha512-rPRDIdaw9q2A0jG03exzPmZe2YemGPgT9RCH6QPFppTl9s4MpiozLZvnyzJxLRXWIj2e3fu5Yq0o02e5EquqKw== + dependencies: + "@lwc/compiler" "8.24.0" + "@lwc/module-resolver" "8.24.0" + "@lwc/shared" "8.24.0" + "@rollup/pluginutils" "~5.3.0" + "@lwc/rollup-plugin@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/rollup-plugin/-/rollup-plugin-8.27.0.tgz#40ea66d6729661da00b70390c379208d62cb1611" @@ -2346,11 +2653,133 @@ "@lwc/shared" "8.27.0" "@rollup/pluginutils" "~5.3.0" +"@lwc/sfdc-compiler-utils@13.2.20": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-compiler-utils/-/sfdc-compiler-utils-13.2.20.tgz#1d5985a7169ce5f6192ede48405a3b53cfa3423f" + integrity sha512-IeorL44CtqnOQhqe8QHqaotOp5uWlraeRJHcRhb0Ssz32LAyIf/4rL+9nleVACfSSiKEyVnkBb7ZTo7TD1edkg== + "@lwc/sfdc-compiler-utils@13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/sfdc-compiler-utils/-/sfdc-compiler-utils-13.3.8.tgz#05f6e08902c275f6bbc96148d1cf9859cb46c34c" integrity sha512-yG7FoI3Y485tZInSft6YQpnxt2ljVGUarjdUEvPDdWNEBPG3G5dgLV94xyQc/Ny/Ji1MDjVSo8tJSIM4UJ4yvA== +"@lwc/sfdc-compiler-utils@13.3.9": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-compiler-utils/-/sfdc-compiler-utils-13.3.9.tgz#51ea9456415c23c4480d6862fe82073453550b1a" + integrity sha512-RVGwJOdkuhUS840RNTN2XtmUbHC6/iIxRUdVwjlM1nZL/bAloBdGzO1olqnbcsyQTey9E06lqoT2LrRH1vBJOg== + +"@lwc/sfdc-lwc-compiler-latest@npm:@lwc/sfdc-lwc-compiler@~13.2.x": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.2.20.tgz#a07a58d08098e752967a70102ff296cd700114a8" + integrity sha512-c1TNMX0lS4K8NdtfEdQSmj1gVYD3Bk+JTikWJK8VyWPN55La+2NZ1+Lf+f0bYUyFOYesJa1OTpiScGLnZzQTmQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/parser" "7.28.5" + "@babel/plugin-syntax-decorators" "7.27.1" + "@babel/preset-typescript" "7.28.5" + "@babel/traverse" "7.28.5" + "@babel/types" "7.28.5" + "@komaci/esm-generator" "260.31.0" + "@lwc/dev-server-plugin-lex" "13.2.20" + "@lwc/eslint-plugin-lwc" "3.3.0" + "@lwc/eslint-plugin-lwc-platform" "6.3.0" + "@lwc/metadata" "13.2.20" + "@lwc/sfdc-compiler-utils" "13.2.20" + "@rollup/plugin-babel" "^6.1.0" + "@rollup/plugin-replace" "^6.0.3" + "@rollup/wasm-node" "4.52.5" + "@salesforce/eslint-config-lwc" "4.1.1" + "@salesforce/eslint-plugin-lightning" "2.0.0" + "@swc/wasm" "1.14.0" + astring "~1.9.0" + doctrine "~3.0.0" + eslint "~9.38.0" + eslint-plugin-import "~2.32.0" + eslint-plugin-jest "~29.0.1" + gray-matter "~4.0.3" + line-column "~1.0.2" + magic-string "~0.30.21" + markdown-it "~14.1.0" + parse5-sax-parser "~8.0.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + terser "~5.44.0" + +"@lwc/sfdc-lwc-compiler-prerelease@npm:@lwc/sfdc-lwc-compiler@~13.3.x": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.3.9.tgz#ec52953b75b8336bc5caa8bb06fc814e6fc74fa2" + integrity sha512-e/L6YRjIu5AboaGOLmn1yECRF5OKvzlgipraqf37iJ4qTRXyly8GSaRZZpkBEuQ9V1CI+03TadexMdQjyIuQGQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/parser" "7.28.5" + "@babel/plugin-syntax-decorators" "7.27.1" + "@babel/preset-typescript" "7.28.5" + "@babel/traverse" "7.28.5" + "@babel/types" "7.28.5" + "@komaci/esm-generator" "260.31.0" + "@lwc/dev-server-plugin-lex" "13.3.9" + "@lwc/eslint-plugin-lwc" "3.3.0" + "@lwc/eslint-plugin-lwc-platform" "6.3.0" + "@lwc/metadata" "13.3.9" + "@lwc/sfdc-compiler-utils" "13.3.9" + "@rollup/plugin-babel" "^6.1.0" + "@rollup/plugin-replace" "^6.0.3" + "@rollup/wasm-node" "4.52.5" + "@salesforce/eslint-config-lwc" "4.1.1" + "@salesforce/eslint-plugin-lightning" "2.0.0" + "@swc/wasm" "1.14.0" + astring "~1.9.0" + doctrine "~3.0.0" + eslint "~9.39.1" + eslint-plugin-import "~2.32.0" + eslint-plugin-jest "~29.0.1" + gray-matter "~4.0.3" + line-column "~1.0.2" + magic-string "~0.30.21" + markdown-it "~14.1.0" + meriyah "^5.0.0" + parse5-sax-parser "~8.0.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + terser "~5.44.0" + +"@lwc/sfdc-lwc-compiler@13.2.20": + version "13.2.20" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.2.20.tgz#a07a58d08098e752967a70102ff296cd700114a8" + integrity sha512-c1TNMX0lS4K8NdtfEdQSmj1gVYD3Bk+JTikWJK8VyWPN55La+2NZ1+Lf+f0bYUyFOYesJa1OTpiScGLnZzQTmQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/parser" "7.28.5" + "@babel/plugin-syntax-decorators" "7.27.1" + "@babel/preset-typescript" "7.28.5" + "@babel/traverse" "7.28.5" + "@babel/types" "7.28.5" + "@komaci/esm-generator" "260.31.0" + "@lwc/dev-server-plugin-lex" "13.2.20" + "@lwc/eslint-plugin-lwc" "3.3.0" + "@lwc/eslint-plugin-lwc-platform" "6.3.0" + "@lwc/metadata" "13.2.20" + "@lwc/sfdc-compiler-utils" "13.2.20" + "@rollup/plugin-babel" "^6.1.0" + "@rollup/plugin-replace" "^6.0.3" + "@rollup/wasm-node" "4.52.5" + "@salesforce/eslint-config-lwc" "4.1.1" + "@salesforce/eslint-plugin-lightning" "2.0.0" + "@swc/wasm" "1.14.0" + astring "~1.9.0" + doctrine "~3.0.0" + eslint "~9.38.0" + eslint-plugin-import "~2.32.0" + eslint-plugin-jest "~29.0.1" + gray-matter "~4.0.3" + line-column "~1.0.2" + magic-string "~0.30.21" + markdown-it "~14.1.0" + parse5-sax-parser "~8.0.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + terser "~5.44.0" + "@lwc/sfdc-lwc-compiler@13.3.8", "@lwc/sfdc-lwc-compiler@~13.3.8": version "13.3.8" resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.3.8.tgz#4235ef3c2124af7b7355478a83b51f7ce9c08b7c" @@ -2389,16 +2818,104 @@ postcss-selector-parser "~7.1.0" terser "~5.44.0" +"@lwc/sfdc-lwc-compiler@13.3.9": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.3.9.tgz#ec52953b75b8336bc5caa8bb06fc814e6fc74fa2" + integrity sha512-e/L6YRjIu5AboaGOLmn1yECRF5OKvzlgipraqf37iJ4qTRXyly8GSaRZZpkBEuQ9V1CI+03TadexMdQjyIuQGQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/parser" "7.28.5" + "@babel/plugin-syntax-decorators" "7.27.1" + "@babel/preset-typescript" "7.28.5" + "@babel/traverse" "7.28.5" + "@babel/types" "7.28.5" + "@komaci/esm-generator" "260.31.0" + "@lwc/dev-server-plugin-lex" "13.3.9" + "@lwc/eslint-plugin-lwc" "3.3.0" + "@lwc/eslint-plugin-lwc-platform" "6.3.0" + "@lwc/metadata" "13.3.9" + "@lwc/sfdc-compiler-utils" "13.3.9" + "@rollup/plugin-babel" "^6.1.0" + "@rollup/plugin-replace" "^6.0.3" + "@rollup/wasm-node" "4.52.5" + "@salesforce/eslint-config-lwc" "4.1.1" + "@salesforce/eslint-plugin-lightning" "2.0.0" + "@swc/wasm" "1.14.0" + astring "~1.9.0" + doctrine "~3.0.0" + eslint "~9.39.1" + eslint-plugin-import "~2.32.0" + eslint-plugin-jest "~29.0.1" + gray-matter "~4.0.3" + line-column "~1.0.2" + magic-string "~0.30.21" + markdown-it "~14.1.0" + meriyah "^5.0.0" + parse5-sax-parser "~8.0.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + terser "~5.44.0" + +"@lwc/shared@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/shared/-/shared-8.23.0.tgz#c9304f7fd8db4256094e5cbf1960dd4f027aa599" + integrity sha512-g6teckOlRJgPkqFJjjrMWoXwEbP3E0PByVIbrxfvv7gN/d8INL+TOA/Deg5ZgRMwuYUGO0Elr5rGcAK5jj/bhA== + +"@lwc/shared@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/shared/-/shared-8.24.0.tgz#2b28aae08502f7755ee6d01a5539fabf194177e2" + integrity sha512-rYgSg5NLS0M3rvX8iOOez1rFy7JjbB/MWPvIuyNi2z2W/CS7oMen1XTW7you8/d0m0kYM7b7S9gMJHzZAuekXQ== + "@lwc/shared@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/shared/-/shared-8.27.0.tgz#182d69110f980640f9bf349fe9112cb7500e7312" integrity sha512-z8KttkAScdsBhHFx6nOXo9F6RVCBPMoTOJ5DIrNZyjf1UKI03wGcBzTLfNpwFB4uxECDZm4RvOtUQ7uVhqxesg== +"@lwc/signals@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/signals/-/signals-8.23.0.tgz#c38177c9ccd20803392a99715e4770a9e9001104" + integrity sha512-mdW1i0i4RBFracnevRN8YQtkUDI/WuWHsQXGQC2kluQAveM/qmVIkvMCPfehBsMwbXpEnYneUEe58XXnuCsAvA== + +"@lwc/signals@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/signals/-/signals-8.24.0.tgz#1b27ba4635bba210f7288503a5192b9140c7b9fe" + integrity sha512-yvW4JRnAORYYrq3l3L//dG++sz5JM2HQ3lHMLpBbBKGWlLflDIUh6teApz8YUddGAyttTo4LcDAajx2HGN+Kfw== + "@lwc/signals@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/signals/-/signals-8.27.0.tgz#1028ea08b47b020a5ca0ea99017e9b5808d95178" integrity sha512-4LRajgmlTLeEE5sNxFp/PBaLUHGUTNldWzkUywSXtFWQIwyUAzovxoEzs5DFlwVuak8bFc1uEcvyBs1XW8P0Eg== +"@lwc/ssr-compiler@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/ssr-compiler/-/ssr-compiler-8.23.0.tgz#cd3ff236701824188e7b9675e927092ffb34d1b2" + integrity sha512-JucwFx+bjVsnyJnfJIbcX2DpaKO+h3vGEBlDPgI6cdaRfymtkxklxZojzc1HTcN+0XSGSiAmBcDn3MKxsNMrMQ== + dependencies: + "@babel/types" "7.28.4" + "@lwc/errors" "8.23.0" + "@lwc/shared" "8.23.0" + "@lwc/template-compiler" "8.23.0" + acorn "8.15.0" + astring "^1.9.0" + estree-toolkit "^1.7.13" + immer "^10.1.3" + meriyah "^5.0.0" + +"@lwc/ssr-compiler@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/ssr-compiler/-/ssr-compiler-8.24.0.tgz#f35793a44f63f203c14b3c1b080dcb00cb067c0f" + integrity sha512-1pnCDuMx6tUrpMKWwq3NVF10B65CILWBen/Axldg5AxIu6TVdlYoy8CbkHcpeS3+Qd9BpAAuQnkZo+RkVEmYww== + dependencies: + "@babel/types" "7.28.5" + "@lwc/errors" "8.24.0" + "@lwc/shared" "8.24.0" + "@lwc/template-compiler" "8.24.0" + acorn "8.15.0" + astring "^1.9.0" + estree-toolkit "^1.7.13" + immer "^10.2.0" + meriyah "^5.0.0" + "@lwc/ssr-compiler@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/ssr-compiler/-/ssr-compiler-8.27.0.tgz#bfd026ba6395c521095cab75588d37d4927b012b" @@ -2414,11 +2931,41 @@ immer "^11.1.0" meriyah "^5.0.0" +"@lwc/ssr-runtime@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/ssr-runtime/-/ssr-runtime-8.23.0.tgz#29da9702f09992a3cf721fa88057c53dcdd7680a" + integrity sha512-J4JSyEGX+DiBUoMIRBUTcrsc0GGI+LuczO4uSLoMIjFQJXjh5dmI058pVBYq5cCXJHTv2vbtUILzQtX3xcFb0A== + +"@lwc/ssr-runtime@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/ssr-runtime/-/ssr-runtime-8.24.0.tgz#def2b8035f6659f9dc46b0442d3cc1ba67ffbcfd" + integrity sha512-ZZezcxHFwPu244ARazQZufXirXmdN5/ExiQcCQ5dqmtrzd+daypXpUYgD3tPbhI+423YMbQbfFxceiri0kL8Tw== + "@lwc/ssr-runtime@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/ssr-runtime/-/ssr-runtime-8.27.0.tgz#d3733f6560d0205d993996e35122a9b989dfb437" integrity sha512-WLJ65Sabcmk6b6/gYS1D1V9Nc8KiY+9/N1UukEjf1XX3wbDMoursofOdb9N4nQ421tTKZipVT/4LzkWsD9cuLA== +"@lwc/style-compiler@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/style-compiler/-/style-compiler-8.23.0.tgz#508faaea6cd4b5df990cc2c0d91b5bbbe3fa905e" + integrity sha512-hIsmMgKyFQ3VSozQtHuU1BcAbbWyk/8BFygB2WdadM/cBrHfNCy+PGLofv8xkyvhDPrfbWBtwFrP9VIRXDdLNA== + dependencies: + "@lwc/shared" "8.23.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + postcss-value-parser "~4.2.0" + +"@lwc/style-compiler@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/style-compiler/-/style-compiler-8.24.0.tgz#c5b7ee9f00370c13828d0c23483ab0ff23ff5100" + integrity sha512-AtlwvAOl8siJLXSb6mxXMw89HCgRWxNhVeYEsIvmCos9HTkdhoG8PX6EYcQj8oDnYFBqe2WcR2AUYRBqwV1iig== + dependencies: + "@lwc/shared" "8.24.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + postcss-value-parser "~4.2.0" + "@lwc/style-compiler@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/style-compiler/-/style-compiler-8.27.0.tgz#8cc9e37ecaaa9e23db489a37b31ea8270e6ea562" @@ -2429,11 +2976,43 @@ postcss-selector-parser "~7.1.1" postcss-value-parser "~4.2.0" +"@lwc/synthetic-shadow@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/synthetic-shadow/-/synthetic-shadow-8.23.0.tgz#b209293ac9e1b03f778b71c802c5f2095e814067" + integrity sha512-wmFB6nMKlsH47+YW+Wr3HdhPdUbHor6yPzbsai85St8+xSlrCzQWuXPWuqv6raFyHg6YnWAiF2Hf5e2h9sdCig== + +"@lwc/synthetic-shadow@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/synthetic-shadow/-/synthetic-shadow-8.24.0.tgz#08ce426ab2762c919204d9963a72575fda2bda64" + integrity sha512-46fWZMRaT7a8fbX/03sXNLB6lBM6xJtIfBivexSUyvNkoyE3iNbRDhzFjTOD1O8Gxx/crpBHaiRtzzEuzcrYtg== + "@lwc/synthetic-shadow@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/synthetic-shadow/-/synthetic-shadow-8.27.0.tgz#fe50864a8982a33c741ffedf30d3ea4c6865e465" integrity sha512-SWOnigcLTTJNm7nxObyNbMUTFw66cCFGRdEBL5j5KT5NlmzC9A2rVWrVuvdFj/h9w45tHt0lz6BzALbOv/IaHQ== +"@lwc/template-compiler@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/template-compiler/-/template-compiler-8.23.0.tgz#323ee9d6476b94421b3041ae359b32ec5bfc0d91" + integrity sha512-E24VtNe4Ej307ui8BuQncBzcd6MdzOjXjrhIOQDnGLzNnGL7I3cuGA2wVwTuV8WNrPg7JkgpJghUduEkN3kubw== + dependencies: + "@lwc/errors" "8.23.0" + "@lwc/shared" "8.23.0" + acorn "~8.15.0" + astring "~1.9.0" + he "~1.2.0" + +"@lwc/template-compiler@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/template-compiler/-/template-compiler-8.24.0.tgz#033a33fdd36debcd9b21565a59a8c97e85964c48" + integrity sha512-x4SBSvDnC75QK4P2YCkAiwbTTeEmQ5RveT7LB6sk6WYma+OiDSk8pFrj5SFrsjtOy6oDxedvIpftlrbUa+Yh0w== + dependencies: + "@lwc/errors" "8.24.0" + "@lwc/shared" "8.24.0" + acorn "~8.15.0" + astring "~1.9.0" + he "~1.2.0" + "@lwc/template-compiler@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/template-compiler/-/template-compiler-8.27.0.tgz#fda60ceda1558edd7281b6cd3f70dd77e5de7c52" @@ -2445,6 +3024,20 @@ astring "~1.9.0" he "~1.2.0" +"@lwc/types@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/types/-/types-8.23.0.tgz#f7a3dca0e4c3a6649dbaf9e41eac9f3b99a6ae86" + integrity sha512-MqRqq/eQu36/lI3MnPn4EAIW5JgYXIorlzpnQYLA6kBnViSCYcaDeJJil/FDIzKSF8HgHf7CuXVJ5MUkcXbqJw== + dependencies: + "@lwc/engine-core" "8.23.0" + +"@lwc/types@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/types/-/types-8.24.0.tgz#20c2b9b10104724f492c429dd292ed7ebb652206" + integrity sha512-yQe8VLfrnZoNxGaf13Hs4XJaqBQ8nrKEYfNHUGI9P1tNPGGr1ooNEFGQfNoBkUU7mxZomXRyOD/moUYOIRjqAw== + dependencies: + "@lwc/engine-core" "8.24.0" + "@lwc/types@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/types/-/types-8.27.0.tgz#74525197cbf82024847eee5ca872a81eebee4a1a" @@ -2452,6 +3045,16 @@ dependencies: "@lwc/engine-core" "8.27.0" +"@lwc/wire-service@8.23.0": + version "8.23.0" + resolved "https://registry.yarnpkg.com/@lwc/wire-service/-/wire-service-8.23.0.tgz#c44f5197d921b5fbfa213cb954cccd11b78b9978" + integrity sha512-vAwzn6gSrC/C0FMIXUWl/Ieyg7xaY4SMoMuBiL36ChvtXfSJjHPhmeVjhMGkkGCXyDHS/3f5/9LhplaIi60EfQ== + +"@lwc/wire-service@8.24.0": + version "8.24.0" + resolved "https://registry.yarnpkg.com/@lwc/wire-service/-/wire-service-8.24.0.tgz#50efa7428bccb710c6fee134ba56e0e84925f02c" + integrity sha512-hpwgttHXCCABfWOy9ChspmpMrTX3HP6fYqEb+Qp85NP52jE1YSUgnncQ/lqDZF5Xak4b2pm7wM1gR6jMDT/jmA== + "@lwc/wire-service@8.27.0": version "8.27.0" resolved "https://registry.yarnpkg.com/@lwc/wire-service/-/wire-service-8.27.0.tgz#32be7e77d23ec30f29a255b59a373b37516c08ee" @@ -6590,6 +7193,46 @@ eslint@^8.56.0, eslint@^8.57.0: strip-ansi "^6.0.1" text-table "^0.2.0" +eslint@~9.38.0: + version "9.38.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.38.0.tgz#3957d2af804e5cf6cc503c618f60acc71acb2e7e" + integrity sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.1" + "@eslint/config-helpers" "^0.4.1" + "@eslint/core" "^0.16.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.38.0" + "@eslint/plugin-kit" "^0.4.0" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + eslint@~9.39.1: version "9.39.2" resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" @@ -7829,6 +8472,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== +immer@^10.1.3, immer@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.2.0.tgz#88a4ce06a1af64172d254b70f7cb04df51c871b1" + integrity sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw== + immer@^11.1.0: version "11.1.3" resolved "https://registry.yarnpkg.com/immer/-/immer-11.1.3.tgz#78681e1deb6cec39753acf04eb16d7576c04f4d6" @@ -8917,6 +9565,54 @@ lunr@^2.3.9: resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== +"lwc-latest@npm:lwc@~8.23.x": + version "8.23.0" + resolved "https://registry.yarnpkg.com/lwc/-/lwc-8.23.0.tgz#1123a559700aa8bb437f54258efa1ed2be8e94f1" + integrity sha512-XeNx83aT0NZJ8ORfR4bHWIgL5m+XoDJvIX0Og+8ZAr9YYMmZJuBd83tmdhrneYXaJTaGbX54TVbvRY90k+/noA== + dependencies: + "@lwc/aria-reflection" "8.23.0" + "@lwc/babel-plugin-component" "8.23.0" + "@lwc/compiler" "8.23.0" + "@lwc/engine-core" "8.23.0" + "@lwc/engine-dom" "8.23.0" + "@lwc/engine-server" "8.23.0" + "@lwc/errors" "8.23.0" + "@lwc/features" "8.23.0" + "@lwc/module-resolver" "8.23.0" + "@lwc/rollup-plugin" "8.23.0" + "@lwc/shared" "8.23.0" + "@lwc/ssr-compiler" "8.23.0" + "@lwc/ssr-runtime" "8.23.0" + "@lwc/style-compiler" "8.23.0" + "@lwc/synthetic-shadow" "8.23.0" + "@lwc/template-compiler" "8.23.0" + "@lwc/types" "8.23.0" + "@lwc/wire-service" "8.23.0" + +"lwc-prerelease@npm:lwc@~8.24.x": + version "8.24.0" + resolved "https://registry.yarnpkg.com/lwc/-/lwc-8.24.0.tgz#fa97aa528c58c813374ce70004af6396747a8b2a" + integrity sha512-u2l1MSulS5W1YIPkeA0ndG2vWFBgAnLGAkYGdbvhEuDdvhIJ7J/U+ftca3f67bx655y1jiBoWjiGEdUDprPcTQ== + dependencies: + "@lwc/aria-reflection" "8.24.0" + "@lwc/babel-plugin-component" "8.24.0" + "@lwc/compiler" "8.24.0" + "@lwc/engine-core" "8.24.0" + "@lwc/engine-dom" "8.24.0" + "@lwc/engine-server" "8.24.0" + "@lwc/errors" "8.24.0" + "@lwc/features" "8.24.0" + "@lwc/module-resolver" "8.24.0" + "@lwc/rollup-plugin" "8.24.0" + "@lwc/shared" "8.24.0" + "@lwc/ssr-compiler" "8.24.0" + "@lwc/ssr-runtime" "8.24.0" + "@lwc/style-compiler" "8.24.0" + "@lwc/synthetic-shadow" "8.24.0" + "@lwc/template-compiler" "8.24.0" + "@lwc/types" "8.24.0" + "@lwc/wire-service" "8.24.0" + lwc@~8.27.0: version "8.27.0" resolved "https://registry.yarnpkg.com/lwc/-/lwc-8.27.0.tgz#6d8ce66e0c014cf22e5a034573ae9296d6406d93" @@ -10557,7 +11253,7 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@~1.22.11: +resolve@~1.22.10, resolve@~1.22.11: version "1.22.11" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== From 5438fb3b2e8cdbf651bd21509553118a57beffc1 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Tue, 13 Jan 2026 15:12:02 -0500 Subject: [PATCH 02/10] fix: tests --- command-snapshot.json | 6 +++--- src/shared/orgUtils.ts | 2 +- test/shared/orgUtils.test.ts | 9 +++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/command-snapshot.json b/command-snapshot.json index 26cc62d9..49f2c50c 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -4,7 +4,7 @@ "command": "lightning:dev:app", "flagAliases": [], "flagChars": ["i", "n", "o", "t"], - "flags": ["device-id", "device-type", "flags-dir", "name", "target-org"], + "flags": ["device-id", "device-type", "flags-dir", "name", "target-org", "version-channel"], "plugin": "@salesforce/plugin-lightning-dev" }, { @@ -12,7 +12,7 @@ "command": "lightning:dev:component", "flagAliases": [], "flagChars": ["c", "n", "o"], - "flags": ["api-version", "client-select", "flags-dir", "json", "name", "target-org"], + "flags": ["api-version", "client-select", "flags-dir", "json", "name", "target-org", "version-channel"], "plugin": "@salesforce/plugin-lightning-dev" }, { @@ -20,7 +20,7 @@ "command": "lightning:dev:site", "flagAliases": [], "flagChars": ["l", "n", "o"], - "flags": ["flags-dir", "get-latest", "guest", "name", "ssr", "target-org"], + "flags": ["flags-dir", "get-latest", "guest", "name", "ssr", "target-org", "version-channel"], "plugin": "@salesforce/plugin-lightning-dev" } ] diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 65bbf7a0..7ce50b10 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -144,7 +144,7 @@ export class OrgUtils { // Priority 2: Environment variable override const envOverride = process.env.FORCE_VERSION_CHANNEL; if (envOverride) { - const validChannels: VersionChannel[] = ['latest', 'prerelease']; + const validChannels: VersionChannel[] = ['latest', 'prerelease', 'next']; if (validChannels.includes(envOverride as VersionChannel)) { return envOverride as VersionChannel; } else { diff --git a/test/shared/orgUtils.test.ts b/test/shared/orgUtils.test.ts index 23d0946b..34868c80 100644 --- a/test/shared/orgUtils.test.ts +++ b/test/shared/orgUtils.test.ts @@ -35,6 +35,10 @@ describe('orgUtils', () => { supportedApiVersions: ['66.0'], dependencies: {}, }, + next: { + supportedApiVersions: ['67.0'], + dependencies: {}, + }, }, defaultChannel: 'latest', }, @@ -61,6 +65,11 @@ describe('orgUtils', () => { const conn = new Connection({ authInfo: new AuthInfo() }); const channel = OrgUtils.getVersionChannel(conn); expect(channel).to.equal('prerelease'); + + process.env.FORCE_VERSION_CHANNEL = 'next'; + const channelNext = OrgUtils.getVersionChannel(conn); + expect(channelNext).to.equal('next'); + delete process.env.FORCE_VERSION_CHANNEL; }); From cffc588aa230d48d5482973622ec2d7d02ae3635 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Tue, 13 Jan 2026 16:50:30 -0500 Subject: [PATCH 03/10] fix: remove check-versions script --- .husky/check-versions.js | 24 ---------- .husky/pre-commit | 3 -- package.json | 4 -- src/commands/lightning/dev/app.ts | 3 +- src/shared/promptUtils.ts | 6 +-- src/shared/versionResolver.ts | 4 +- yarn.lock | 76 +++++++++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 39 deletions(-) delete mode 100644 .husky/check-versions.js diff --git a/.husky/check-versions.js b/.husky/check-versions.js deleted file mode 100644 index 40321fee..00000000 --- a/.husky/check-versions.js +++ /dev/null @@ -1,24 +0,0 @@ -import fs from 'node:fs'; -import semver from 'semver'; - -// Read package.json -const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); - -// Extract versions -const devServerDependencyVersion = packageJson.dependencies['@lwc/lwc-dev-server']; -const devServerTargetVersionRange = packageJson.apiVersionMetadata?.target?.matchingDevServerVersion; - -if (!devServerDependencyVersion || !devServerTargetVersionRange) { - console.error('Error: missing @lwc/lwc-dev-server or matchingDevServerVersion'); - process.exit(1); // Fail the check -} - -// Compare versions -if (semver.intersects(devServerTargetVersionRange, devServerDependencyVersion)) { - 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 ${devServerTargetVersionRange} 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 26e6a547..4fbfe02f 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,7 +1,4 @@ #!/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/package.json b/package.json index baafe273..8bd9207a 100644 --- a/package.json +++ b/package.json @@ -272,10 +272,6 @@ } }, "defaultChannel": "latest", - "target": { - "versionNumber": "67.0", - "matchingDevServerVersion": "~13.3.8" - }, "versionToTagMappings": [ { "versionNumber": "62.0", diff --git a/src/commands/lightning/dev/app.ts b/src/commands/lightning/dev/app.ts index 4158c285..cbde7e97 100644 --- a/src/commands/lightning/dev/app.ts +++ b/src/commands/lightning/dev/app.ts @@ -286,8 +286,7 @@ export default class LightningDevApp extends SfCommand { this.spinner.start(messages.getMessage('spinner.extract.archive')); const outputDir = path.dirname(bundlePath); const finalBundlePath = path.join(outputDir, 'Chatter.app'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger as any); + await CommonUtils.extractZIPArchive(bundlePath, outputDir, logger); this.spinner.stop(); bundlePath = finalBundlePath; } diff --git a/src/shared/promptUtils.ts b/src/shared/promptUtils.ts index 34999e37..c60784c8 100644 --- a/src/shared/promptUtils.ts +++ b/src/shared/promptUtils.ts @@ -79,10 +79,8 @@ export class PromptUtils { ): Promise { const availableDevices = platform === Platform.ios - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - await new AppleDeviceManager(logger as any).enumerateDevices() - : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - await new AndroidDeviceManager(logger as any).enumerateDevices(); + ? await new AppleDeviceManager(logger).enumerateDevices() + : await new AndroidDeviceManager(logger).enumerateDevices(); if (!availableDevices || availableDevices.length === 0) { throw new Error(messages.getMessage('error.device.enumeration')); diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts index 4a48f291..e0ec1f13 100644 --- a/src/shared/versionResolver.ts +++ b/src/shared/versionResolver.ts @@ -41,7 +41,7 @@ type PackageJson = { channels: { [key in VersionChannel]: ChannelConfig; }; - defaultChannel: string; + defaultChannel: VersionChannel; }; }; @@ -116,7 +116,7 @@ export class VersionResolver { */ public static getDefaultChannel(): VersionChannel { const packageJson = this.getPackageJson(); - return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; + return packageJson.apiVersionMetadata.defaultChannel; } /** diff --git a/yarn.lock b/yarn.lock index 7b63b48a..764b2875 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2520,6 +2520,20 @@ glob "^11.0.3" ws "^8.18.3" +"@lwc/lwc-dev-server-next@npm:@lwc/lwc-dev-server@~13.3.x": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server/-/lwc-dev-server-13.3.9.tgz#4e922c079738d9b0a3cba64e301a357bc479ca45" + integrity sha512-4HfMSacB0wXnn8tXjb2SIokswjEx3z5bdGQvqCuycF6RPtNel+KZE/4gBtjZtuwRHMXTO8GeyoPmFPTWLi8aVQ== + dependencies: + "@lwc/lwc-dev-server-types" "13.3.9" + "@lwc/sfdc-lwc-compiler" "13.3.9" + chalk "~5.6.2" + chokidar "~3.6.0" + commander "~14.0.2" + fast-xml-parser "^5.3.0" + glob "^11.0.3" + ws "^8.18.3" + "@lwc/lwc-dev-server-prerelease@npm:@lwc/lwc-dev-server@~13.3.x": version "13.3.9" resolved "https://registry.yarnpkg.com/@lwc/lwc-dev-server/-/lwc-dev-server-13.3.9.tgz#4e922c079738d9b0a3cba64e301a357bc479ca45" @@ -2705,6 +2719,44 @@ postcss-selector-parser "~7.1.0" terser "~5.44.0" +"@lwc/sfdc-lwc-compiler-next@npm:@lwc/sfdc-lwc-compiler@~13.3.x": + version "13.3.9" + resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.3.9.tgz#ec52953b75b8336bc5caa8bb06fc814e6fc74fa2" + integrity sha512-e/L6YRjIu5AboaGOLmn1yECRF5OKvzlgipraqf37iJ4qTRXyly8GSaRZZpkBEuQ9V1CI+03TadexMdQjyIuQGQ== + dependencies: + "@babel/core" "7.28.5" + "@babel/parser" "7.28.5" + "@babel/plugin-syntax-decorators" "7.27.1" + "@babel/preset-typescript" "7.28.5" + "@babel/traverse" "7.28.5" + "@babel/types" "7.28.5" + "@komaci/esm-generator" "260.31.0" + "@lwc/dev-server-plugin-lex" "13.3.9" + "@lwc/eslint-plugin-lwc" "3.3.0" + "@lwc/eslint-plugin-lwc-platform" "6.3.0" + "@lwc/metadata" "13.3.9" + "@lwc/sfdc-compiler-utils" "13.3.9" + "@rollup/plugin-babel" "^6.1.0" + "@rollup/plugin-replace" "^6.0.3" + "@rollup/wasm-node" "4.52.5" + "@salesforce/eslint-config-lwc" "4.1.1" + "@salesforce/eslint-plugin-lightning" "2.0.0" + "@swc/wasm" "1.14.0" + astring "~1.9.0" + doctrine "~3.0.0" + eslint "~9.39.1" + eslint-plugin-import "~2.32.0" + eslint-plugin-jest "~29.0.1" + gray-matter "~4.0.3" + line-column "~1.0.2" + magic-string "~0.30.21" + markdown-it "~14.1.0" + meriyah "^5.0.0" + parse5-sax-parser "~8.0.0" + postcss "~8.5.6" + postcss-selector-parser "~7.1.0" + terser "~5.44.0" + "@lwc/sfdc-lwc-compiler-prerelease@npm:@lwc/sfdc-lwc-compiler@~13.3.x": version "13.3.9" resolved "https://registry.yarnpkg.com/@lwc/sfdc-lwc-compiler/-/sfdc-lwc-compiler-13.3.9.tgz#ec52953b75b8336bc5caa8bb06fc814e6fc74fa2" @@ -9589,6 +9641,30 @@ lunr@^2.3.9: "@lwc/types" "8.23.0" "@lwc/wire-service" "8.23.0" +"lwc-next@npm:lwc@~8.24.x": + version "8.24.0" + resolved "https://registry.yarnpkg.com/lwc/-/lwc-8.24.0.tgz#fa97aa528c58c813374ce70004af6396747a8b2a" + integrity sha512-u2l1MSulS5W1YIPkeA0ndG2vWFBgAnLGAkYGdbvhEuDdvhIJ7J/U+ftca3f67bx655y1jiBoWjiGEdUDprPcTQ== + dependencies: + "@lwc/aria-reflection" "8.24.0" + "@lwc/babel-plugin-component" "8.24.0" + "@lwc/compiler" "8.24.0" + "@lwc/engine-core" "8.24.0" + "@lwc/engine-dom" "8.24.0" + "@lwc/engine-server" "8.24.0" + "@lwc/errors" "8.24.0" + "@lwc/features" "8.24.0" + "@lwc/module-resolver" "8.24.0" + "@lwc/rollup-plugin" "8.24.0" + "@lwc/shared" "8.24.0" + "@lwc/ssr-compiler" "8.24.0" + "@lwc/ssr-runtime" "8.24.0" + "@lwc/style-compiler" "8.24.0" + "@lwc/synthetic-shadow" "8.24.0" + "@lwc/template-compiler" "8.24.0" + "@lwc/types" "8.24.0" + "@lwc/wire-service" "8.24.0" + "lwc-prerelease@npm:lwc@~8.24.x": version "8.24.0" resolved "https://registry.yarnpkg.com/lwc/-/lwc-8.24.0.tgz#fa97aa528c58c813374ce70004af6396747a8b2a" From 1fc902c3059b6466fadbd48a61437bba84970285 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Tue, 13 Jan 2026 17:13:31 -0500 Subject: [PATCH 04/10] fix: types --- src/shared/previewUtils.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index 6c58ad36..caaec8ce 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -57,8 +57,7 @@ export class PreviewUtils { ports: { httpPort: number; httpsPort: number }, logger?: Logger ): string { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger as any); + return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger); } /** @@ -110,10 +109,8 @@ export class PreviewUtils { device = platform === Platform.ios - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - await new AppleDeviceManager(logger as any).getDevice(deviceId) - : // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - await new AndroidDeviceManager(logger as any).getDevice(deviceId); + ? await new AppleDeviceManager(logger).getDevice(deviceId) + : await new AndroidDeviceManager(logger).getDevice(deviceId); } else { logger?.debug('Prompting the user to select a device.'); @@ -399,8 +396,7 @@ export class PreviewUtils { return new Promise((resolve, reject) => { if (progress && totalSize) { - response.body?.on('data', (chunk) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + response.body?.on('data', (chunk: string) => { downloadedSize += chunk.length; const percentage = parseFloat(Math.min((downloadedSize / totalSize) * 100, 100).toFixed(1)); progress.update(percentage); From 7b6cde76dbdd1206bbc2ba5dee712cecdc3a3f4c Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Tue, 13 Jan 2026 18:06:38 -0500 Subject: [PATCH 05/10] fix: simplify versionresolver --- package.json | 10 +- src/shared/orgUtils.ts | 16 +-- src/shared/typeUtils.ts | 23 ++++ src/shared/versionResolver.ts | 194 +++++++--------------------- test/shared/orgUtils.test.ts | 29 ----- test/shared/versionResolver.test.ts | 90 ++----------- test/tsconfig.json | 5 +- tsconfig.json | 5 +- yarn.lock | 83 +++++------- 9 files changed, 131 insertions(+), 324 deletions(-) create mode 100644 src/shared/typeUtils.ts diff --git a/package.json b/package.json index 8bd9207a..afcc67b7 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,12 @@ "@inquirer/select": "^2.4.7", "@lwc/lwc-dev-server": "~13.3.8", "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.2.x", - "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x", "@lwc/lwc-dev-server-next": "npm:@lwc/lwc-dev-server@~13.3.x", + "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x", "@lwc/sfdc-lwc-compiler": "~13.3.8", "@lwc/sfdc-lwc-compiler-latest": "npm:@lwc/sfdc-lwc-compiler@~13.2.x", - "@lwc/sfdc-lwc-compiler-prerelease": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", "@lwc/sfdc-lwc-compiler-next": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", + "@lwc/sfdc-lwc-compiler-prerelease": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", "@lwrjs/api": "0.18.3", "@oclif/core": "^4.5.6", "@salesforce/core": "^8.24.0", @@ -25,8 +25,8 @@ "glob": "^13.0.0", "lwc": "~8.27.0", "lwc-latest": "npm:lwc@~8.23.x", - "lwc-prerelease": "npm:lwc@~8.24.x", "lwc-next": "npm:lwc@~8.24.x", + "lwc-prerelease": "npm:lwc@~8.24.x", "node-fetch": "^3.3.2", "open": "^10.2.0", "xml2js": "^0.6.2" @@ -121,7 +121,9 @@ "access": "public" }, "resolutions": { - "cliui": "7.0.4" + "cliui": "7.0.4", + "prettier": "^3.7.4", + "pretty-quick": "^4.2.2" }, "wireit": { "build": { diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 7ce50b10..5805cef2 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -15,7 +15,7 @@ */ import { Connection } from '@salesforce/core'; -import { VersionChannel, VersionResolver } from './versionResolver.js'; +import { VersionChannel, resolveChannel, getDefaultChannel } from './versionResolver.js'; type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -77,7 +77,7 @@ export class OrgUtils { const appMenuItemsQuery = 'SELECT Label,Description,Name FROM AppMenuItem WHERE IsAccessible=true AND IsVisible=true'; const appMenuItems = await connection.query<{ Label: string; Description: string; Name: string }>( - appMenuItemsQuery + appMenuItemsQuery, ); const appDefinitionsQuery = "SELECT DeveloperName,DurableId FROM AppDefinition WHERE UiType='Lightning'"; @@ -149,31 +149,27 @@ export class OrgUtils { return envOverride as VersionChannel; } else { throw new Error( - `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}` + `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}`, ); } } // Priority 3: Skip check for testing (legacy compatibility) if (process.env.SKIP_API_VERSION_CHECK === 'true') { - return VersionResolver.getDefaultChannel(); + return getDefaultChannel(); } // Priority 4: Automatic detection based on org version const orgVersion = connection.version; try { - const orgId = connection.getAuthInfoFields().orgId; - if (!orgId) { - throw new Error('Could not determine org ID from connection.'); - } - return VersionResolver.resolveChannelWithCache(orgId, orgVersion); + return resolveChannel(orgVersion); } catch (error) { // Enhance error with helpful message throw new Error( `${error instanceof Error ? error.message : String(error)}\n` + `Your org is on API version ${orgVersion}. ` + - 'Please ensure you are using the correct version of the CLI and this plugin.' + 'Please ensure you are using the correct version of the CLI and this plugin.', ); } } diff --git a/src/shared/typeUtils.ts b/src/shared/typeUtils.ts new file mode 100644 index 00000000..3e7b81b4 --- /dev/null +++ b/src/shared/typeUtils.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type ValueOf = T[keyof T]; +type Entries = Array<[keyof T, ValueOf]>; + +// Same as `Object.entries()` but with type inference +export function objectEntries(obj: T): Entries { + return Object.entries(obj) as Entries; +} diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts index e0ec1f13..740d8414 100644 --- a/src/shared/versionResolver.ts +++ b/src/shared/versionResolver.ts @@ -14,170 +14,68 @@ * limitations under the License. */ -import path from 'node:path'; -import url from 'node:url'; -import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; +import packageJson from '../../package.json' with { type: 'json' }; +import { objectEntries } from './typeUtils.js'; /** * Resolves org API version to appropriate dependency channel */ -export type VersionChannel = 'latest' | 'prerelease' | 'next'; +export type VersionChannel = keyof typeof packageJson.apiVersionMetadata.channels; -export type ChannelConfig = { - supportedApiVersions: string[]; - dependencies: { - [key: string]: string; - }; -}; - -type CacheEntry = { - apiVersion: string; - channel: VersionChannel; - timestamp: number; -}; +/** + * Extracts major.minor from a version string (e.g., "65.0" from "65.0.1") + */ +function getMajorMinor(version: string): string { + const parts = version.split('.'); + return `${parts[0]}.${parts[1]}`; +} -type PackageJson = { - apiVersionMetadata: { - channels: { - [key in VersionChannel]: ChannelConfig; - }; - defaultChannel: VersionChannel; - }; -}; +/** + * Returns a formatted list of all supported API versions + */ +function getSupportedVersionsList(): string { + const channels = packageJson.apiVersionMetadata.channels; + const allVersions: string[] = []; -export class VersionResolver { - private static channelMetadata: Map | null = null; - private static versionCache: Map = new Map(); - private static readonly CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + for (const config of Object.values(channels)) { + allVersions.push(...config.supportedApiVersions); + } - /** - * Given an org API version, returns the appropriate channel - * - * @param orgApiVersion - The API version from the org (e.g., "65.0") - * @returns The channel to use ('latest' or 'prerelease') - * @throws Error if the API version is not supported by any channel - */ - public static resolveChannel(orgApiVersion: string): VersionChannel { - const channels = this.loadChannelMetadata(); + return allVersions.join(', '); +} - for (const [channel, config] of channels.entries()) { - if (config.supportedApiVersions.includes(orgApiVersion)) { - return channel; - } - } +/** + * Given an org API version, returns the appropriate channel + * + * @param orgApiVersion - The API version from the org (e.g., "65.0") + * @returns The channel to use ('latest' or 'prerelease') + * @throws Error if the API version is not supported by any channel + */ +export function resolveChannel(orgApiVersion: string): VersionChannel { + const channels = packageJson.apiVersionMetadata.channels; - // If no exact match, try to find by major.minor comparison - const orgMajorMinor = this.getMajorMinor(orgApiVersion); - for (const [channel, config] of channels.entries()) { - for (const supportedVersion of config.supportedApiVersions) { - if (this.getMajorMinor(supportedVersion) === orgMajorMinor) { - return channel; - } - } + for (const [channel, config] of objectEntries(channels)) { + if (config.supportedApiVersions.includes(orgApiVersion)) { + return channel; } - - throw new Error( - `Unsupported org API version: ${orgApiVersion}. This plugin supports: ${this.getSupportedVersionsList()}` - ); } - /** - * Resolves channel with caching support - * - * @param orgId - Unique identifier for the org - * @param orgApiVersion - The API version from the org - * @returns The channel to use - */ - public static resolveChannelWithCache(orgId: string, orgApiVersion: string): VersionChannel { - // Check cache first - const cached = this.versionCache.get(orgId); - if (cached) { - const age = Date.now() - cached.timestamp; - if (age < this.CACHE_TTL_MS && cached.apiVersion === orgApiVersion) { - return cached.channel; + // If no exact match, try to find by major.minor comparison + const orgMajorMinor = getMajorMinor(orgApiVersion); + for (const [channel, config] of objectEntries(channels)) { + for (const supportedVersion of config.supportedApiVersions) { + if (getMajorMinor(supportedVersion) === orgMajorMinor) { + return channel; } - // Cache expired or version changed, remove it - this.versionCache.delete(orgId); - } - - // Resolve and cache - const channel = this.resolveChannel(orgApiVersion); - this.versionCache.set(orgId, { - apiVersion: orgApiVersion, - channel, - timestamp: Date.now(), - }); - - return channel; - } - - /** - * Returns the default channel from package.json - */ - public static getDefaultChannel(): VersionChannel { - const packageJson = this.getPackageJson(); - return packageJson.apiVersionMetadata.defaultChannel; - } - - /** - * Clears the version cache (useful for testing or when orgs are upgraded) - */ - public static clearCache(): void { - this.versionCache.clear(); - this.channelMetadata = null; - } - - /** - * Removes a specific org from the cache - */ - public static removeCacheEntry(orgId: string): void { - this.versionCache.delete(orgId); - } - - /** - * Loads channel metadata from package.json - */ - private static loadChannelMetadata(): Map { - if (this.channelMetadata) { - return this.channelMetadata; } - - const packageJson = this.getPackageJson(); - const channels = packageJson.apiVersionMetadata.channels; - - this.channelMetadata = new Map(); - for (const [channel, config] of Object.entries(channels)) { - this.channelMetadata.set(channel as VersionChannel, config); - } - - return this.channelMetadata; } - /** - * Extracts major.minor from a version string (e.g., "65.0" from "65.0.1") - */ - private static getMajorMinor(version: string): string { - const parts = version.split('.'); - return `${parts[0]}.${parts[1]}`; - } - - /** - * Returns a formatted list of all supported API versions - */ - private static getSupportedVersionsList(): string { - const channels = this.loadChannelMetadata(); - const allVersions: string[] = []; - - for (const config of channels.values()) { - allVersions.push(...config.supportedApiVersions); - } - - return allVersions.join(', '); - } + throw new Error(`Unsupported org API version: ${orgApiVersion}. This plugin supports: ${getSupportedVersionsList()}`); +} - private static getPackageJson(): PackageJson { - const dirname = path.dirname(url.fileURLToPath(import.meta.url)); - const packageJsonFilePath = path.resolve(dirname, '../../package.json'); - return CommonUtils.loadJsonFromFile(packageJsonFilePath) as unknown as PackageJson; - } +/** + * Returns the default channel from package.json + */ +export function getDefaultChannel(): VersionChannel { + return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; } diff --git a/test/shared/orgUtils.test.ts b/test/shared/orgUtils.test.ts index 34868c80..c6596f98 100644 --- a/test/shared/orgUtils.test.ts +++ b/test/shared/orgUtils.test.ts @@ -17,38 +17,11 @@ import { TestContext } from '@salesforce/core/testSetup'; import { expect } from 'chai'; import { AuthInfo, Connection } from '@salesforce/core'; -import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; import { OrgUtils } from '../../src/shared/orgUtils.js'; -import { VersionResolver } from '../../src/shared/versionResolver.js'; describe('orgUtils', () => { const $$ = new TestContext(); - const mockPackageJson = { - apiVersionMetadata: { - channels: { - latest: { - supportedApiVersions: ['65.0'], - dependencies: {}, - }, - prerelease: { - supportedApiVersions: ['66.0'], - dependencies: {}, - }, - next: { - supportedApiVersions: ['67.0'], - dependencies: {}, - }, - }, - defaultChannel: 'latest', - }, - }; - - beforeEach(() => { - $$.SANDBOX.stub(CommonUtils, 'loadJsonFromFile').returns(mockPackageJson); - VersionResolver.clearCache(); - }); - afterEach(() => { $$.restore(); }); @@ -91,7 +64,6 @@ describe('orgUtils', () => { it('auto-detects channel based on org version', async () => { const conn = new Connection({ authInfo: new AuthInfo() }); $$.SANDBOX.stub(conn, 'version').get(() => '65.0'); - $$.SANDBOX.stub(conn, 'getAuthInfoFields').returns({ orgId: 'org1' }); const channel = OrgUtils.getVersionChannel(conn); expect(channel).to.equal('latest'); @@ -100,7 +72,6 @@ describe('orgUtils', () => { it('throws error for unsupported org version', async () => { const conn = new Connection({ authInfo: new AuthInfo() }); $$.SANDBOX.stub(conn, 'version').get(() => '64.0'); - $$.SANDBOX.stub(conn, 'getAuthInfoFields').returns({ orgId: 'org1' }); expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Unsupported org API version: 64.0/); }); diff --git a/test/shared/versionResolver.test.ts b/test/shared/versionResolver.test.ts index e16c7c57..3e116120 100644 --- a/test/shared/versionResolver.test.ts +++ b/test/shared/versionResolver.test.ts @@ -14,99 +14,27 @@ * limitations under the License. */ -import { TestContext } from '@salesforce/core/testSetup'; import { expect } from 'chai'; -import { CommonUtils } from '@salesforce/lwc-dev-mobile-core'; -import { VersionResolver } from '../../src/shared/versionResolver.js'; +import { resolveChannel, getDefaultChannel } from '../../src/shared/versionResolver.js'; describe('VersionResolver', () => { - const $$ = new TestContext(); - - const mockPackageJson = { - apiVersionMetadata: { - channels: { - latest: { - supportedApiVersions: ['65.0'], - dependencies: { - '@lwc/lwc-dev-server': '~13.2.x', - lwc: '~8.23.x', - }, - }, - prerelease: { - supportedApiVersions: ['66.0'], - dependencies: { - '@lwc/lwc-dev-server': '~13.3.x', - lwc: '~8.24.x', - }, - }, - next: { - supportedApiVersions: ['67.0'], - dependencies: { - '@lwc/lwc-dev-server': '~13.3.x', - lwc: '~8.24.x', - }, - }, - }, - defaultChannel: 'latest', - }, - }; - - beforeEach(() => { - $$.SANDBOX.stub(CommonUtils, 'loadJsonFromFile').returns(mockPackageJson); - VersionResolver.clearCache(); - }); - - afterEach(() => { - $$.restore(); - }); - it('resolveChannel returns correct channel for exact match', () => { - expect(VersionResolver.resolveChannel('65.0')).to.equal('latest'); - expect(VersionResolver.resolveChannel('66.0')).to.equal('prerelease'); - expect(VersionResolver.resolveChannel('67.0')).to.equal('next'); + expect(resolveChannel('65.0')).to.equal('latest'); + expect(resolveChannel('66.0')).to.equal('prerelease'); + expect(resolveChannel('67.0')).to.equal('next'); }); it('resolveChannel returns correct channel for major.minor match', () => { - expect(VersionResolver.resolveChannel('65.0.1')).to.equal('latest'); - expect(VersionResolver.resolveChannel('66.0.5')).to.equal('prerelease'); - expect(VersionResolver.resolveChannel('67.0.2')).to.equal('next'); + expect(resolveChannel('65.0.1')).to.equal('latest'); + expect(resolveChannel('66.0.5')).to.equal('prerelease'); + expect(resolveChannel('67.0.2')).to.equal('next'); }); it('resolveChannel throws error for unsupported version', () => { - expect(() => VersionResolver.resolveChannel('64.0')).to.throw(/Unsupported org API version: 64.0/); - }); - - it('resolveChannelWithCache returns cached value', () => { - const resolveSpy = $$.SANDBOX.spy(VersionResolver, 'resolveChannel'); - - // First call - resolves - const channel1 = VersionResolver.resolveChannelWithCache('org1', '65.0'); - expect(channel1).to.equal('latest'); - expect(resolveSpy.calledOnce).to.be.true; - - // Second call - cached - const channel2 = VersionResolver.resolveChannelWithCache('org1', '65.0'); - expect(channel2).to.equal('latest'); - expect(resolveSpy.calledOnce).to.be.true; - - // Different org - resolves - const channel3 = VersionResolver.resolveChannelWithCache('org2', '65.0'); - expect(channel3).to.equal('latest'); - expect(resolveSpy.calledTwice).to.be.true; - }); - - it('resolveChannelWithCache invalidates cache when version changes', () => { - VersionResolver.resolveChannelWithCache('org1', '65.0'); - - const resolveSpy = $$.SANDBOX.spy(VersionResolver, 'resolveChannel'); - - // Version changed - re-resolves - const channel = VersionResolver.resolveChannelWithCache('org1', '66.0'); - expect(channel).to.equal('prerelease'); - expect(resolveSpy.calledOnce).to.be.true; + expect(() => resolveChannel('64.0')).to.throw(/Unsupported org API version: 64.0/); }); it('getDefaultChannel returns default from package.json', () => { - expect(VersionResolver.getDefaultChannel()).to.equal('latest'); + expect(getDefaultChannel()).to.equal('latest'); }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index 7183d88b..8e058f14 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,6 +2,9 @@ "extends": "@salesforce/dev-config/tsconfig-test-strict-esm", "include": ["./**/*.ts", "./**/*.nut.ts"], "compilerOptions": { - "skipLibCheck": true + "skipLibCheck": true, + "resolveJsonModule": true, + "module": "nodenext", + "moduleResolution": "nodenext" } } diff --git a/tsconfig.json b/tsconfig.json index 1fa9d631..8dc5c245 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,13 @@ { "extends": "@salesforce/dev-config/tsconfig-strict-esm", "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", "outDir": "lib", "rootDir": "src", "skipLibCheck": true, - "baseUrl": "." + "baseUrl": ".", + "resolveJsonModule": true }, "include": ["./src/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index 764b2875..09bd626d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3896,6 +3896,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/core@^0.2.7": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.2.9.tgz#d229a7b7f9dac167a156992ef23c7f023653f53b" + integrity sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA== + "@pnpm/config.env-replace@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz#ab29da53df41e8948a00f2433f085f54de8b3a4c" @@ -7425,21 +7430,6 @@ events@^3.3.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -7924,13 +7914,6 @@ get-stdin@^9.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== -get-stream@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -8470,11 +8453,6 @@ https-proxy-agent@^7.0.1: agent-base "^7.0.2" debug "4" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -8509,7 +8487,7 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.0: +ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -8519,6 +8497,11 @@ ignore@^7.0.3: resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.3.tgz#397ef9315dfe0595671eefe8b633fec6943ab733" integrity sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA== +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -10345,7 +10328,7 @@ normalize-url@^8.0.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== -npm-run-path@^4.0.0, npm-run-path@^4.0.1: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -10784,7 +10767,7 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.1: +picocolors@^1.0.1, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -10794,11 +10777,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516" - integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== - picomatch@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" @@ -10913,23 +10891,23 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@^2.8.8: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^2.8.8, prettier@^3.7.4: + version "3.7.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f" + integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA== -pretty-quick@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-3.3.1.tgz#cfde97fec77a8d201a0e0c9c71d9990e12587ee2" - integrity sha512-3b36UXfYQ+IXXqex6mCca89jC8u0mYLqFAN5eTQKoXO6oCQYcIVYZEB/5AlBHI7JPYygReM2Vv6Vom/Gln7fBg== +pretty-quick@^3.3.1, pretty-quick@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-4.2.2.tgz#0fc31da666f182fe14e119905fc9829b5b85a234" + integrity sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w== dependencies: - execa "^4.1.0" - find-up "^4.1.0" - ignore "^5.3.0" + "@pkgr/core" "^0.2.7" + ignore "^7.0.5" mri "^1.2.0" - picocolors "^1.0.0" - picomatch "^3.0.1" - tslib "^2.6.2" + picocolors "^1.1.1" + picomatch "^4.0.2" + tinyexec "^0.3.2" + tslib "^2.8.1" process-nextick-args@~2.0.0: version "2.0.1" @@ -12216,6 +12194,11 @@ tiny-jsonc@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-jsonc/-/tiny-jsonc-1.0.2.tgz#208df4c437684199cc724f31c2b91ee39c349678" integrity sha512-f5QDAfLq6zIVSyCZQZhhyl0QS6MvAyTxgz4X4x3+EoCktNWEYJ6PeoEA97fyb98njpBNNi88ybpD7m+BDFXaCw== +tinyexec@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.2.tgz#941794e657a85e496577995c6eef66f53f42b3d2" + integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== + tinyglobby@^0.2.14, tinyglobby@^0.2.9: version "0.2.14" resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.14.tgz#5280b0cf3f972b050e74ae88406c0a6a58f4079d" @@ -12330,7 +12313,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.6.2, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 86efeb1a7c6c45ed482525563f0618c9595d0e06 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Wed, 14 Jan 2026 14:10:19 -0500 Subject: [PATCH 06/10] fix: simplify dependency loader --- src/lwc-dev-server/index.ts | 8 +- src/shared/dependencyLoader.ts | 121 +++++++++++---------------- test/shared/dependencyLoader.test.ts | 15 ++-- 3 files changed, 58 insertions(+), 86 deletions(-) diff --git a/src/lwc-dev-server/index.ts b/src/lwc-dev-server/index.ts index 8f69e75c..15c942e3 100644 --- a/src/lwc-dev-server/index.ts +++ b/src/lwc-dev-server/index.ts @@ -19,7 +19,7 @@ import type { LWCServer, ServerConfig, Workspace } from '@lwc/lwc-dev-server'; import { Connection, Lifecycle, Logger, SfProject } from '@salesforce/core'; import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; import { glob } from 'glob'; -import { DependencyLoader } from '../shared/dependencyLoader.js'; +import { loadLwcDevServer } from '../shared/dependencyLoader.js'; import { OrgUtils } from '../shared/orgUtils.js'; import { VersionChannel } from '../shared/versionResolver.js'; import { @@ -34,7 +34,7 @@ async function createLWCServerConfig( clientType: string, serverPorts?: { httpPort: number; httpsPort: number }, certData?: SSLCertificateData, - workspace?: Workspace + workspace?: Workspace, ): Promise { const project = await SfProject.resolve(); const packageDirs = project.getPackageDirectories(); @@ -85,12 +85,12 @@ export async function startLWCServer( serverPorts?: { httpPort: number; httpsPort: number }, certData?: SSLCertificateData, workspace?: Workspace, - versionChannelOverride?: VersionChannel + versionChannelOverride?: VersionChannel, ): Promise { const channel = OrgUtils.getVersionChannel(connection, versionChannelOverride); logger.trace(`Using version channel: ${channel}`); - const lwcDevServerModule = await DependencyLoader.loadLwcDevServer(channel); + const lwcDevServerModule = await loadLwcDevServer(channel); const config = await createLWCServerConfig(rootDir, token, clientType, serverPorts, certData, workspace); diff --git a/src/shared/dependencyLoader.ts b/src/shared/dependencyLoader.ts index 5cc30bbe..5288f61a 100644 --- a/src/shared/dependencyLoader.ts +++ b/src/shared/dependencyLoader.ts @@ -27,85 +27,62 @@ export type LwcDevServerModule = { }; /** - * Dynamically loads LWC dependencies based on version channel + * Loads the LWC dev server module for the specified channel + * Uses dynamic import to load the aliased package at runtime + * + * @param channel - The version channel ('latest', 'prerelease', or 'next') + * @returns The loaded module */ -export class DependencyLoader { - private static loadedModules: Map = new Map(); - - /** - * Loads the LWC dev server module for the specified channel - * Uses dynamic import to load the aliased package at runtime - * - * @param channel - The version channel ('latest' or 'prerelease') - * @returns The loaded module - */ - public static async loadLwcDevServer(channel: VersionChannel): Promise { - // Check cache first - if (this.loadedModules.has(channel)) { - return this.loadedModules.get(channel)!; - } +export async function loadLwcDevServer(channel: VersionChannel): Promise { + const packageName = `@lwc/lwc-dev-server-${channel}`; - // Construct the aliased package name - const packageName = `@lwc/lwc-dev-server-${channel}`; - - try { - // Dynamic import of the aliased package - const module = (await import(packageName)) as LwcDevServerModule; - this.loadedModules.set(channel, module); - return module; - } catch (error) { - throw new Error( - `Failed to load LWC dev server for channel '${channel}'. ` + - `Package '${packageName}' could not be imported. ` + - `Error: ${error instanceof Error ? error.message : String(error)}` - ); - } + try { + return (await import(packageName)) as LwcDevServerModule; + } catch (error) { + throw new Error( + `Failed to load LWC dev server for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); } +} - /** - * Loads the LWC compiler module for the specified channel - * - * @param channel - The version channel ('latest' or 'prerelease') - * @returns The loaded compiler module - */ - public static async loadLwcCompiler(channel: VersionChannel): Promise { - const packageName = `@lwc/sfdc-lwc-compiler-${channel}`; +/** + * Loads the LWC compiler module for the specified channel + * + * @param channel - The version channel ('latest', 'prerelease', or 'next') + * @returns The loaded compiler module + */ +export async function loadLwcCompiler(channel: VersionChannel): Promise { + const packageName = `@lwc/sfdc-lwc-compiler-${channel}`; - try { - return (await import(packageName)) as unknown; - } catch (error) { - throw new Error( - `Failed to load LWC compiler for channel '${channel}'. ` + - `Package '${packageName}' could not be imported. ` + - `Error: ${error instanceof Error ? error.message : String(error)}` - ); - } + try { + return (await import(packageName)) as unknown; + } catch (error) { + throw new Error( + `Failed to load LWC compiler for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); } +} - /** - * Loads the base LWC module for the specified channel - * - * @param channel - The version channel ('latest' or 'prerelease') - * @returns The loaded LWC module - */ - public static async loadLwc(channel: VersionChannel): Promise { - const packageName = `lwc-${channel}`; - - try { - return (await import(packageName)) as unknown; - } catch (error) { - throw new Error( - `Failed to load LWC for channel '${channel}'. ` + - `Package '${packageName}' could not be imported. ` + - `Error: ${error instanceof Error ? error.message : String(error)}` - ); - } - } +/** + * Loads the base LWC module for the specified channel + * + * @param channel - The version channel ('latest', 'prerelease', or 'next') + * @returns The loaded LWC module + */ +export async function loadLwc(channel: VersionChannel): Promise { + const packageName = `lwc-${channel}`; - /** - * Clears the module cache (useful for testing) - */ - public static clearCache(): void { - this.loadedModules.clear(); + try { + return (await import(packageName)) as unknown; + } catch (error) { + throw new Error( + `Failed to load LWC for channel '${channel}'. ` + + `Package '${packageName}' could not be imported. ` + + `Error: ${error instanceof Error ? error.message : String(error)}`, + ); } } diff --git a/test/shared/dependencyLoader.test.ts b/test/shared/dependencyLoader.test.ts index 6c0689d9..5211aded 100644 --- a/test/shared/dependencyLoader.test.ts +++ b/test/shared/dependencyLoader.test.ts @@ -15,18 +15,13 @@ */ import { expect } from 'chai'; -import { DependencyLoader } from '../../src/shared/dependencyLoader.js'; +import { loadLwcDevServer, loadLwcCompiler, loadLwc } from '../../src/shared/dependencyLoader.js'; describe('DependencyLoader', () => { - beforeEach(() => { - DependencyLoader.clearCache(); - }); - it('exists and has expected methods', () => { - expect(typeof DependencyLoader.loadLwcDevServer).to.equal('function'); - expect(typeof DependencyLoader.loadLwcCompiler).to.equal('function'); - expect(typeof DependencyLoader.loadLwc).to.equal('function'); - expect(typeof DependencyLoader.clearCache).to.equal('function'); + expect(typeof loadLwcDevServer).to.equal('function'); + expect(typeof loadLwcCompiler).to.equal('function'); + expect(typeof loadLwc).to.equal('function'); }); it('loads the aliased package (real import call)', async () => { @@ -34,7 +29,7 @@ describe('DependencyLoader', () => { // However, loading LWC modules in Node might still trigger ReferenceErrors if browser globals are missing. // We use a try-catch to handle both cases and just verify the attempt was made. try { - const module = await DependencyLoader.loadLwcDevServer('latest'); + const module = await loadLwcDevServer('latest'); expect(module).to.exist; } catch (error) { // If it fails with a ReferenceError or similar, it's still "working" in terms of From fc024f4817239017c8444944b277879eec5aa23f Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Wed, 14 Jan 2026 14:20:26 -0500 Subject: [PATCH 07/10] fix: remove duplication --- src/shared/orgUtils.ts | 4 ++-- src/shared/typeUtils.ts | 17 ++++++++++++++++- src/shared/versionResolver.ts | 9 ++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 5805cef2..436e93f6 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -15,7 +15,7 @@ */ import { Connection } from '@salesforce/core'; -import { VersionChannel, resolveChannel, getDefaultChannel } from './versionResolver.js'; +import { VersionChannel, resolveChannel, getDefaultChannel, getAllChannels } from './versionResolver.js'; type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -144,7 +144,7 @@ export class OrgUtils { // Priority 2: Environment variable override const envOverride = process.env.FORCE_VERSION_CHANNEL; if (envOverride) { - const validChannels: VersionChannel[] = ['latest', 'prerelease', 'next']; + const validChannels = getAllChannels(); if (validChannels.includes(envOverride as VersionChannel)) { return envOverride as VersionChannel; } else { diff --git a/src/shared/typeUtils.ts b/src/shared/typeUtils.ts index 3e7b81b4..23f8a539 100644 --- a/src/shared/typeUtils.ts +++ b/src/shared/typeUtils.ts @@ -17,7 +17,22 @@ type ValueOf = T[keyof T]; type Entries = Array<[keyof T, ValueOf]>; -// Same as `Object.entries()` but with type inference +/** + * Same as `Object.entries()` but with type inference + * + * @param obj the object to get the entries of + * @returns the entries of the object + */ export function objectEntries(obj: T): Entries { return Object.entries(obj) as Entries; } + +/** + * Same as `Object.keys()` but with type inference + * + * @param obj the object to get the keys of + * @returns the keys of the object + */ +export function objectKeys(obj: T): Array { + return Object.keys(obj) as unknown as Array; +} diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts index 740d8414..46b991a6 100644 --- a/src/shared/versionResolver.ts +++ b/src/shared/versionResolver.ts @@ -15,7 +15,7 @@ */ import packageJson from '../../package.json' with { type: 'json' }; -import { objectEntries } from './typeUtils.js'; +import { objectEntries, objectKeys } from './typeUtils.js'; /** * Resolves org API version to appropriate dependency channel @@ -79,3 +79,10 @@ export function resolveChannel(orgApiVersion: string): VersionChannel { export function getDefaultChannel(): VersionChannel { return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; } + +/** + * Returns a list of all valid version channels + */ +export function getAllChannels(): VersionChannel[] { + return objectKeys(packageJson.apiVersionMetadata.channels); +} From 0fcd4afb711947f375f761ae0b6ad92749bd94ed Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Wed, 14 Jan 2026 14:46:17 -0500 Subject: [PATCH 08/10] fix: remove deprecated ensureMatchingAPIVersion --- src/shared/orgUtils.ts | 12 ------------ test/commands/lightning/dev/app.test.ts | 22 +++++++++++----------- test/shared/previewUtils.test.ts | 24 ++++++++++++------------ 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 436e93f6..928005d1 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -173,16 +173,4 @@ export class OrgUtils { ); } } - - /** - * 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 - * @deprecated Use getVersionChannel instead - */ - public static ensureMatchingAPIVersion(connection: Connection): void { - this.getVersionChannel(connection); - } } diff --git a/test/commands/lightning/dev/app.test.ts b/test/commands/lightning/dev/app.test.ts index 8dfac728..cf48a868 100644 --- a/test/commands/lightning/dev/app.test.ts +++ b/test/commands/lightning/dev/app.test.ts @@ -62,7 +62,7 @@ describe('lightning dev app', () => { 'iPhone 15 Pro Max', DeviceType.mobile, AppleOSType.iOS, - new Version(17, 5, 0) + new Version(17, 5, 0), ); const testAndroidDevice = new AndroidDevice( 'Pixel_5_API_34', @@ -70,7 +70,7 @@ describe('lightning dev app', () => { DeviceType.mobile, AndroidOSType.googleAPIs, new Version(34, 0, 0), - false + false, ); const certData: SSLCertificateData = { derCertificate: Buffer.from('A', 'utf-8'), @@ -103,7 +103,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(); + $$.SANDBOX.stub(OrgUtils, 'getVersionChannel').returns('latest'); MockedLightningPreviewApp = await esmock('../../../../src/commands/lightning/dev/app.js', { '../../../../src/lwc-dev-server/index.js': { @@ -153,7 +153,7 @@ describe('lightning dev app', () => { try { $$.SANDBOX.stub(OrgUtils, 'getAppDefinitionDurableId').resolves(testAppDefinition.DurableId); $$.SANDBOX.stub(PreviewUtils, 'generateWebSocketUrlForLocalDevServer').throws( - new Error('Cannot determine LDP url.') + new Error('Cannot determine LDP url.'), ); await MockedLightningPreviewApp.run(['--name', 'Sales', '-o', testOrgData.username, '-t', Platform.desktop]); } catch (err) { @@ -175,7 +175,7 @@ describe('lightning dev app', () => { it('prompts user to select lightning app when not provided', async () => { const promptStub = $$.SANDBOX.stub(PromptUtils, 'promptUserToSelectLightningExperienceApp').resolves( - testAppDefinition + testAppDefinition, ); await verifyOrgOpen(`lightning/app/${testAppDefinition.DurableId}`, Platform.desktop); expect(promptStub.calledOnce); @@ -245,7 +245,7 @@ describe('lightning dev app', () => { $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); $$.SANDBOX.stub(PreviewUtils, 'getMobileDevice').callsFake((platform) => - Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice) + Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice), ); await verifyMobileThrowsWhenDeviceFailsToBoot(Platform.ios); @@ -260,7 +260,7 @@ describe('lightning dev app', () => { $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); $$.SANDBOX.stub(PreviewUtils, 'getMobileDevice').callsFake((platform) => - Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice) + Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice), ); $$.SANDBOX.stub(AppleDevice.prototype, 'boot').resolves(); @@ -280,7 +280,7 @@ describe('lightning dev app', () => { $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); $$.SANDBOX.stub(PreviewUtils, 'getMobileDevice').callsFake((platform) => - Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice) + Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice), ); $$.SANDBOX.stub(PreviewUtils, 'generateSelfSignedCert').resolves(certData); @@ -315,7 +315,7 @@ describe('lightning dev app', () => { $$.SANDBOX.stub(LwcDevMobileCoreSetup.prototype, 'run').resolves(); $$.SANDBOX.stub(PreviewUtils, 'getMobileDevice').callsFake((platform) => - Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice) + Promise.resolve(platform === Platform.ios ? testIOSDevice : testAndroidDevice), ); $$.SANDBOX.stub(PreviewUtils, 'generateSelfSignedCert').resolves(certData); @@ -416,11 +416,11 @@ describe('lightning dev app', () => { expectedLdpServerUrl, testLdpServerId, 'Sales', - testAppDefinition.DurableId + testAppDefinition.DurableId, ); const downloadStub = $$.SANDBOX.stub(PreviewUtils, 'downloadSalesforceMobileAppBundle').resolves( - testBundleArchive + testBundleArchive, ); const extractStub = $$.SANDBOX.stub(CommonUtils, 'extractZIPArchive').resolves(); const installStub = diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index ca365428..00ba979f 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -49,7 +49,7 @@ describe('previewUtils', () => { 'iPhone 15 Pro Max', DeviceType.mobile, AppleOSType.iOS, - new Version(17, 5, 0) + new Version(17, 5, 0), ); const testAndroidDevice = new AndroidDevice( 'Pixel_5_API_34', @@ -57,7 +57,7 @@ describe('previewUtils', () => { DeviceType.mobile, AndroidOSType.googleAPIs, new Version(34, 0, 0), - false + false, ); const testUsername = 'SalesforceDeveloper'; @@ -126,8 +126,8 @@ describe('previewUtils', () => { testLdpServerId, 'MyAppId', 'MyTargetOrg', - 'MyAuraMode' - ) + 'MyAuraMode', + ), ).to.deep.equal([ '--path', `lightning/app/MyAppId?0.aura.ldpServerUrl=MyLdpServerUrl&0.aura.ldpServerId=${testLdpServerId}&0.aura.mode=MyAuraMode`, @@ -148,8 +148,8 @@ describe('previewUtils', () => { testLdpServerId, 'MyAppName', 'MyAppId', - 'MyAuraMode' - ) + 'MyAuraMode', + ), ).to.deep.equal([ { name: 'LightningExperienceAppName', value: 'MyAppName' }, { name: 'LightningExperienceAppID', value: 'MyAppId' }, @@ -210,7 +210,7 @@ describe('previewUtils', () => { 'https://localhost:3333', testLdpServerId, 'myTestComponent', - 'myTargetOrg' + 'myTargetOrg', ); const parsed = parseArgs({ @@ -232,7 +232,7 @@ describe('previewUtils', () => { 'https://localhost:3333', testLdpServerId, undefined, - 'myTargetOrg' + 'myTargetOrg', ); const parsed = parseArgs({ @@ -253,7 +253,7 @@ describe('previewUtils', () => { const result = PreviewUtils.generateComponentPreviewLaunchArguments( 'https://localhost:3333', testLdpServerId, - 'myTestComponent' + 'myTestComponent', ); const parsed = parseArgs({ @@ -324,7 +324,7 @@ describe('previewUtils', () => { const generateWebSocketUrlStub = $$.SANDBOX.stub( LwcDevMobileCorePreviewUtils, - 'generateWebSocketUrlForLocalDevServer' + 'generateWebSocketUrlForLocalDevServer', ).returns(mockUrl); // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any @@ -343,7 +343,7 @@ describe('previewUtils', () => { } as Org; $$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true); - $$.SANDBOX.stub(OrgUtils, 'ensureMatchingAPIVersion').returns(); + $$.SANDBOX.stub(OrgUtils, 'getVersionChannel').returns('latest'); $$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(testIdentityData); const result = await PreviewUtils.initializePreviewConnection(mockOrg); @@ -398,7 +398,7 @@ describe('previewUtils', () => { }; $$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true); - $$.SANDBOX.stub(OrgUtils, 'ensureMatchingAPIVersion').returns(); + $$.SANDBOX.stub(OrgUtils, 'getVersionChannel').returns('latest'); $$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(identityDataWithoutEntityId); try { From 2637a8788d3d0f7bdf5b2722ce82b0dd1ee80b73 Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Wed, 14 Jan 2026 15:00:08 -0500 Subject: [PATCH 09/10] refactor: simplify VersionResolver and DependencyLoader and improve error handling --- messages/shared.utils.md | 4 ++++ src/shared/orgUtils.ts | 31 ++++++++++++++++++++----------- src/shared/versionResolver.ts | 2 +- test/shared/orgUtils.test.ts | 2 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/messages/shared.utils.md b/messages/shared.utils.md index 165eed20..f8ff5eac 100644 --- a/messages/shared.utils.md +++ b/messages/shared.utils.md @@ -50,6 +50,10 @@ Couldn't find identity data while generating preview arguments Couldn't find entity ID while generating preview arguments +# error.org.api-unsupported + +Your org is on API Version %s. This version of the plugin supports only %s. Please update your plugin. + # error.no-project This command is required to run from within a Salesforce project directory. %s diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index 928005d1..cc9928d8 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -14,8 +14,17 @@ * limitations under the License. */ -import { Connection } from '@salesforce/core'; -import { VersionChannel, resolveChannel, getDefaultChannel, getAllChannels } from './versionResolver.js'; +import { Connection, Messages, Logger } from '@salesforce/core'; +import { + VersionChannel, + resolveChannel, + getDefaultChannel, + getAllChannels, + getSupportedVersionsList, +} from './versionResolver.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils'); type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -33,6 +42,8 @@ export type AppDefinition = { * the local dev server matches with Org API versions, we rely on defining a metadata section in package.json */ export class OrgUtils { + private static logger = Logger.childFromRoot('OrgUtils'); + /** * Given an app name, it queries the AppDefinition table in the org to find * the DurableId for the app. To do so, it will first attempt at finding the @@ -148,9 +159,10 @@ export class OrgUtils { if (validChannels.includes(envOverride as VersionChannel)) { return envOverride as VersionChannel; } else { - throw new Error( - `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}`, - ); + const message = + `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}`; + this.logger.error(message); + throw new Error(message); } } @@ -165,12 +177,9 @@ export class OrgUtils { try { return resolveChannel(orgVersion); } catch (error) { - // Enhance error with helpful message - throw new Error( - `${error instanceof Error ? error.message : String(error)}\n` + - `Your org is on API version ${orgVersion}. ` + - 'Please ensure you are using the correct version of the CLI and this plugin.', - ); + const message = messages.getMessage('error.org.api-unsupported', [orgVersion, getSupportedVersionsList()]); + this.logger.error(message); + throw new Error(message); } } } diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts index 46b991a6..a6888f1b 100644 --- a/src/shared/versionResolver.ts +++ b/src/shared/versionResolver.ts @@ -33,7 +33,7 @@ function getMajorMinor(version: string): string { /** * Returns a formatted list of all supported API versions */ -function getSupportedVersionsList(): string { +export function getSupportedVersionsList(): string { const channels = packageJson.apiVersionMetadata.channels; const allVersions: string[] = []; diff --git a/test/shared/orgUtils.test.ts b/test/shared/orgUtils.test.ts index c6596f98..ae806a91 100644 --- a/test/shared/orgUtils.test.ts +++ b/test/shared/orgUtils.test.ts @@ -73,7 +73,7 @@ describe('orgUtils', () => { const conn = new Connection({ authInfo: new AuthInfo() }); $$.SANDBOX.stub(conn, 'version').get(() => '64.0'); - expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Unsupported org API version: 64.0/); + expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Your org is on API Version 64.0/); }); }); From ea205866f1056bdbd3c31bc163a905b0d87f011b Mon Sep 17 00:00:00 2001 From: Mohammed Abdul Sattar Date: Thu, 15 Jan 2026 14:44:10 -0500 Subject: [PATCH 10/10] feat: implement dual version support via NPM aliasing and dynamic loading - Added aliased dependencies for @lwc/lwc-dev-server, @lwc/sfdc-lwc-compiler, and lwc - Implemented DependencyLoader for dynamic runtime branching based on org API version - Removed 'version channel' concept in favor of direct API version mapping - Updated CLI commands to pass org connection for version detection - Added .husky/check-versions.js to ensure dependency/metadata alignment - Cleaned up unused VersionResolver and type utilities --- .husky/check-versions.js | 66 +++++++++++++++++ .husky/pre-commit | 3 + command-snapshot.json | 6 +- messages/lightning.dev.app.md | 8 --- messages/lightning.dev.component.md | 8 --- messages/lightning.dev.site.md | 8 --- package.json | 94 ++++++++----------------- src/commands/lightning/dev/app.ts | 21 +----- src/commands/lightning/dev/component.ts | 18 ++--- src/commands/lightning/dev/site.ts | 24 +------ src/lwc-dev-server/index.ts | 9 +-- src/shared/dependencyLoader.ts | 93 ++++++++++++++---------- src/shared/orgUtils.ts | 59 +--------------- src/shared/previewUtils.ts | 22 +++--- src/shared/typeUtils.ts | 38 ---------- src/shared/versionResolver.ts | 88 ----------------------- src/types/aliased-deps.d.ts | 18 ++--- test/commands/lightning/dev/app.test.ts | 1 - test/shared/dependencyLoader.test.ts | 4 +- test/shared/orgUtils.test.ts | 51 -------------- test/shared/previewUtils.test.ts | 2 - test/shared/versionResolver.test.ts | 40 ----------- 22 files changed, 192 insertions(+), 489 deletions(-) create mode 100644 .husky/check-versions.js delete mode 100644 src/shared/typeUtils.ts delete mode 100644 src/shared/versionResolver.ts delete mode 100644 test/shared/versionResolver.test.ts diff --git a/.husky/check-versions.js b/.husky/check-versions.js new file mode 100644 index 00000000..fc5d5297 --- /dev/null +++ b/.husky/check-versions.js @@ -0,0 +1,66 @@ +import fs from 'node:fs'; +import semver from 'semver'; + +/** + * This script ensures that the aliased dependencies in package.json stay in sync + * with the versions defined in apiVersionMetadata. + */ + +// Read package.json +const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); +const apiVersionMetadata = packageJson.apiVersionMetadata; + +if (!apiVersionMetadata) { + console.error('Error: missing apiVersionMetadata in package.json'); + process.exit(1); +} + +let hasError = false; + +// Iterate through each API version defined in metadata +for (const [apiVersion, metadata] of Object.entries(apiVersionMetadata)) { + const expectedDeps = metadata.dependencies; + if (!expectedDeps) continue; + + for (const [depName, expectedRange] of Object.entries(expectedDeps)) { + // For each dependency in metadata, find its aliased counterpart in package.json dependencies + // e.g. @lwc/lwc-dev-server -> @lwc/lwc-dev-server-65.0 + const aliasName = `${depName}-${apiVersion}`; + const actualAliasValue = packageJson.dependencies[aliasName]; + + if (!actualAliasValue) { + console.error(`Error: Missing aliased dependency '${aliasName}' in package.json for API version ${apiVersion}`); + hasError = true; + continue; + } + + // actualAliasValue looks like "npm:@lwc/lwc-dev-server@~13.2.x" or "npm:lwc@~8.23.x" + // We want to extract the version range after the last @ + const match = actualAliasValue.match(/@([^@]+)$/); + if (!match) { + console.error(`Error: Could not parse version range from aliased dependency '${aliasName}': ${actualAliasValue}`); + hasError = true; + continue; + } + + const actualRange = match[1]; + + // Compare the range in metadata with the range in the aliased dependency + if (!semver.intersects(expectedRange, actualRange)) { + console.error( + `Error: Version mismatch for '${aliasName}'. ` + + `Expected ${expectedRange} in apiVersionMetadata, but found ${actualRange} in dependencies.`, + ); + hasError = true; + } + } +} + +if (hasError) { + console.error( + '\nWhen updating LWC dependencies, you must ensure that the versions in apiVersionMetadata match the aliased dependencies in package.json.', + ); + process.exit(1); +} + +process.exit(0); diff --git a/.husky/pre-commit b/.husky/pre-commit index 4fbfe02f..26e6a547 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/command-snapshot.json b/command-snapshot.json index 49f2c50c..26cc62d9 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -4,7 +4,7 @@ "command": "lightning:dev:app", "flagAliases": [], "flagChars": ["i", "n", "o", "t"], - "flags": ["device-id", "device-type", "flags-dir", "name", "target-org", "version-channel"], + "flags": ["device-id", "device-type", "flags-dir", "name", "target-org"], "plugin": "@salesforce/plugin-lightning-dev" }, { @@ -12,7 +12,7 @@ "command": "lightning:dev:component", "flagAliases": [], "flagChars": ["c", "n", "o"], - "flags": ["api-version", "client-select", "flags-dir", "json", "name", "target-org", "version-channel"], + "flags": ["api-version", "client-select", "flags-dir", "json", "name", "target-org"], "plugin": "@salesforce/plugin-lightning-dev" }, { @@ -20,7 +20,7 @@ "command": "lightning:dev:site", "flagAliases": [], "flagChars": ["l", "n", "o"], - "flags": ["flags-dir", "get-latest", "guest", "name", "ssr", "target-org", "version-channel"], + "flags": ["flags-dir", "get-latest", "guest", "name", "ssr", "target-org"], "plugin": "@salesforce/plugin-lightning-dev" } ] diff --git a/messages/lightning.dev.app.md b/messages/lightning.dev.app.md index 58b6620d..ce3765f9 100644 --- a/messages/lightning.dev.app.md +++ b/messages/lightning.dev.app.md @@ -31,14 +31,6 @@ Type of device to display the app preview. ID of the mobile device to display the preview if device type is set to `ios` or `android`. The default value is the ID of the first available mobile device. -# flags.version-channel.summary - -Manually specify which version channel to use (latest, prerelease, or next). - -# flags.version-channel.description - -Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". - # error.fetching.app-id Unable to determine App Id for %s diff --git a/messages/lightning.dev.component.md b/messages/lightning.dev.component.md index 0abc94a8..6fc3aab0 100644 --- a/messages/lightning.dev.component.md +++ b/messages/lightning.dev.component.md @@ -24,14 +24,6 @@ Name of a component to preview. Launch component preview without selecting a component -# flags.version-channel.summary - -Manually specify which version channel to use (latest, prerelease, or next). - -# flags.version-channel.description - -Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". - # error.directory Unable to find components diff --git a/messages/lightning.dev.site.md b/messages/lightning.dev.site.md index 9a0811b2..20fd1011 100644 --- a/messages/lightning.dev.site.md +++ b/messages/lightning.dev.site.md @@ -33,14 +33,6 @@ Preview the site as a guest user (rather than an authenticated user). Preview the SSR bundle -# flags.version-channel.summary - -Manually specify which version channel to use (latest, prerelease, or next). - -# flags.version-channel.description - -Override automatic version detection and force a specific dependency channel. Useful for testing and debugging. Valid values: "latest", "prerelease", "next". - # examples - Select a site to preview from the org "myOrg": diff --git a/package.json b/package.json index afcc67b7..f5b4d35f 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "@inquirer/prompts": "^5.3.8", "@inquirer/select": "^2.4.7", "@lwc/lwc-dev-server": "~13.3.8", - "@lwc/lwc-dev-server-latest": "npm:@lwc/lwc-dev-server@~13.2.x", - "@lwc/lwc-dev-server-next": "npm:@lwc/lwc-dev-server@~13.3.x", - "@lwc/lwc-dev-server-prerelease": "npm:@lwc/lwc-dev-server@~13.3.x", + "@lwc/lwc-dev-server-65.0": "npm:@lwc/lwc-dev-server@~13.2.x", + "@lwc/lwc-dev-server-66.0": "npm:@lwc/lwc-dev-server@~13.3.x", + "@lwc/lwc-dev-server-67.0": "npm:@lwc/lwc-dev-server@~13.3.x", "@lwc/sfdc-lwc-compiler": "~13.3.8", - "@lwc/sfdc-lwc-compiler-latest": "npm:@lwc/sfdc-lwc-compiler@~13.2.x", - "@lwc/sfdc-lwc-compiler-next": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", - "@lwc/sfdc-lwc-compiler-prerelease": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", + "@lwc/sfdc-lwc-compiler-65.0": "npm:@lwc/sfdc-lwc-compiler@~13.2.x", + "@lwc/sfdc-lwc-compiler-66.0": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", + "@lwc/sfdc-lwc-compiler-67.0": "npm:@lwc/sfdc-lwc-compiler@~13.3.x", "@lwrjs/api": "0.18.3", "@oclif/core": "^4.5.6", "@salesforce/core": "^8.24.0", @@ -24,9 +24,9 @@ "axios": "^1.13.2", "glob": "^13.0.0", "lwc": "~8.27.0", - "lwc-latest": "npm:lwc@~8.23.x", - "lwc-next": "npm:lwc@~8.24.x", - "lwc-prerelease": "npm:lwc@~8.24.x", + "lwc-65.0": "npm:lwc@~8.23.x", + "lwc-66.0": "npm:lwc@~8.24.x", + "lwc-67.0": "npm:lwc@~8.24.x", "node-fetch": "^3.3.2", "open": "^10.2.0", "xml2js": "^0.6.2" @@ -52,7 +52,7 @@ "typescript": "^5.5.4" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "files": [ "/lib", @@ -241,65 +241,27 @@ } }, "apiVersionMetadata": { - "channels": { - "latest": { - "supportedApiVersions": [ - "65.0" - ], - "dependencies": { - "@lwc/lwc-dev-server": "~13.2.x", - "@lwc/sfdc-lwc-compiler": "~13.2.x", - "lwc": "~8.23.x" - } - }, - "prerelease": { - "supportedApiVersions": [ - "66.0" - ], - "dependencies": { - "@lwc/lwc-dev-server": "~13.3.x", - "@lwc/sfdc-lwc-compiler": "~13.3.x", - "lwc": "~8.24.x" - } - }, - "next": { - "supportedApiVersions": [ - "67.0" - ], - "dependencies": { - "@lwc/lwc-dev-server": "~13.3.x", - "@lwc/sfdc-lwc-compiler": "~13.3.x", - "lwc": "~8.24.x" - } + "65.0": { + "dependencies": { + "@lwc/lwc-dev-server": "~13.2.x", + "@lwc/sfdc-lwc-compiler": "~13.2.x", + "lwc": "~8.23.x" } }, - "defaultChannel": "latest", - "versionToTagMappings": [ - { - "versionNumber": "62.0", - "tagName": "v1" - }, - { - "versionNumber": "63.0", - "tagName": "v2" - }, - { - "versionNumber": "64.0", - "tagName": "v3" - }, - { - "versionNumber": "65.0", - "tagName": "latest" - }, - { - "versionNumber": "66.0", - "tagName": "prerelease" - }, - { - "versionNumber": "67.0", - "tagName": "next" + "66.0": { + "dependencies": { + "@lwc/lwc-dev-server": "~13.3.x", + "@lwc/sfdc-lwc-compiler": "~13.3.x", + "lwc": "~8.24.x" } - ] + }, + "67.0": { + "dependencies": { + "@lwc/lwc-dev-server": "~13.3.x", + "@lwc/sfdc-lwc-compiler": "~13.3.x", + "lwc": "~8.24.x" + } + } }, "exports": "./lib/index.js", "type": "module", diff --git a/src/commands/lightning/dev/app.ts b/src/commands/lightning/dev/app.ts index cbde7e97..7ce7be13 100644 --- a/src/commands/lightning/dev/app.ts +++ b/src/commands/lightning/dev/app.ts @@ -30,7 +30,6 @@ import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; import { PromptUtils } from '../../../shared/promptUtils.js'; import { MetaUtils } from '../../../shared/metaUtils.js'; -import { VersionChannel } from '../../../shared/versionResolver.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.app'); @@ -70,12 +69,6 @@ export default class LightningDevApp extends SfCommand { summary: messages.getMessage('flags.device-id.summary'), char: 'i', }), - 'version-channel': Flags.string({ - summary: messages.getMessage('flags.version-channel.summary'), - description: messages.getMessage('flags.version-channel.description'), - options: ['latest', 'prerelease', 'next'], - required: false, - }), }; public async run(): Promise { @@ -118,8 +111,6 @@ export default class LightningDevApp extends SfCommand { const ldpServerUrl = PreviewUtils.generateWebSocketUrlForLocalDevServer(platform, serverPorts, logger); logger.debug(`Local Dev Server url is ${ldpServerUrl}`); - const versionChannel = flags['version-channel'] as VersionChannel | undefined; - if (platform === Platform.desktop) { await this.desktopPreview( targetOrg, @@ -130,7 +121,6 @@ export default class LightningDevApp extends SfCommand { ldpServerUrl, appId, logger, - versionChannel ); } else { await this.mobilePreview( @@ -145,7 +135,6 @@ export default class LightningDevApp extends SfCommand { appId, deviceId, logger, - versionChannel ); } } @@ -159,7 +148,6 @@ export default class LightningDevApp extends SfCommand { ldpServerUrl: string, appId: string | undefined, logger: Logger, - versionChannelOverride?: VersionChannel ): Promise { if (!appId) { logger.debug('No Lightning Experience application name provided.... using the default app instead.'); @@ -175,7 +163,7 @@ export default class LightningDevApp extends SfCommand { ldpServerUrl, ldpServerId, appId, - targetOrgArg + targetOrgArg, ); // Start the LWC Dev Server @@ -188,7 +176,6 @@ export default class LightningDevApp extends SfCommand { serverPorts, undefined, undefined, - versionChannelOverride ); // Open the browser and navigate to the right page @@ -207,7 +194,6 @@ export default class LightningDevApp extends SfCommand { appId: string | undefined, deviceId: string | undefined, logger: Logger, - versionChannelOverride?: VersionChannel ): Promise { try { // Verify that user environment is set up for mobile (i.e. has right tooling) @@ -278,7 +264,7 @@ export default class LightningDevApp extends SfCommand { platform, logger, this.spinner, - this.progress + this.progress, ); // on iOS the bundle comes as a ZIP archive so we need to extract it first @@ -307,7 +293,6 @@ export default class LightningDevApp extends SfCommand { serverPorts, certData, undefined, - versionChannelOverride ); // Launch the native app for previewing (launchMobileApp will show its own spinner) @@ -316,7 +301,7 @@ export default class LightningDevApp extends SfCommand { ldpServerUrl, ldpServerId, appName, - appId + appId, ); const targetActivity = (appConfig as AndroidAppPreviewConfig)?.activity; const targetApp = targetActivity ? `${appConfig.id}/${targetActivity}` : appConfig.id; diff --git a/src/commands/lightning/dev/component.ts b/src/commands/lightning/dev/component.ts index 76ae5eab..a2f6288f 100644 --- a/src/commands/lightning/dev/component.ts +++ b/src/commands/lightning/dev/component.ts @@ -23,7 +23,6 @@ import { PromptUtils } from '../../../shared/promptUtils.js'; import { PreviewUtils } from '../../../shared/previewUtils.js'; import { startLWCServer } from '../../../lwc-dev-server/index.js'; import { MetaUtils } from '../../../shared/metaUtils.js'; -import { VersionChannel } from '../../../shared/versionResolver.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'lightning.dev.component'); @@ -55,12 +54,6 @@ export default class LightningDevComponent extends SfCommand { @@ -73,7 +66,7 @@ export default class LightningDevComponent extends SfCommand !!component); if (componentName) { // validate that the component exists before launching the server const match = components.find( - (component) => componentName === component.name || componentName === component.label + (component) => componentName === component.name || componentName === component.label, ); if (!match) { throw new Error(messages.getMessage('error.component-not-found', [componentName])); @@ -179,7 +172,6 @@ export default class LightningDevComponent extends SfCommand { summary: messages.getMessage('flags.ssr.summary'), default: false, }), - 'version-channel': Flags.string({ - summary: messages.getMessage('flags.version-channel.summary'), - description: messages.getMessage('flags.version-channel.description'), - options: ['latest', 'prerelease', 'next'], - required: false, - }), }; public async run(): Promise { @@ -90,8 +83,6 @@ export default class LightningDevSite extends SfCommand { throw new Error(sharedMessages.getMessage('error.localdev.not.enabled')); } - OrgUtils.getVersionChannel(connection, flags['version-channel'] as VersionChannel | undefined); - // If user doesn't specify a site, prompt the user for one if (!siteName) { const allSites = await ExperienceSite.getAllExpSites(org); @@ -101,11 +92,7 @@ export default class LightningDevSite extends SfCommand { const selectedSite = new ExperienceSite(org, siteName); if (!ssr) { - return await this.openPreviewUrl( - selectedSite, - connection, - flags['version-channel'] as VersionChannel | undefined - ); + return await this.openPreviewUrl(selectedSite, connection); } await this.serveSSRSite(selectedSite, getLatest, siteName, guest); } catch (e) { @@ -118,7 +105,7 @@ export default class LightningDevSite extends SfCommand { selectedSite: ExperienceSite, getLatest: boolean, siteName: string, - guest: boolean + guest: boolean, ): Promise { let siteZip: string | undefined; @@ -174,11 +161,7 @@ export default class LightningDevSite extends SfCommand { } } - private async openPreviewUrl( - selectedSite: ExperienceSite, - connection: Connection, - versionChannelOverride?: VersionChannel - ): Promise { + private async openPreviewUrl(selectedSite: ExperienceSite, connection: Connection): Promise { let sfdxProjectRootPath = ''; try { sfdxProjectRootPath = await SfProject.resolveProjectPath(); @@ -217,7 +200,6 @@ export default class LightningDevSite extends SfCommand { serverPorts, undefined, undefined, - versionChannelOverride ); const url = new URL(previewUrl); url.searchParams.set('aura.ldpServerUrl', ldpServerUrl); diff --git a/src/lwc-dev-server/index.ts b/src/lwc-dev-server/index.ts index 15c942e3..58364d9e 100644 --- a/src/lwc-dev-server/index.ts +++ b/src/lwc-dev-server/index.ts @@ -20,8 +20,6 @@ import { Connection, Lifecycle, Logger, SfProject } from '@salesforce/core'; import { SSLCertificateData } from '@salesforce/lwc-dev-mobile-core'; import { glob } from 'glob'; import { loadLwcDevServer } from '../shared/dependencyLoader.js'; -import { OrgUtils } from '../shared/orgUtils.js'; -import { VersionChannel } from '../shared/versionResolver.js'; import { ConfigUtils, LOCAL_DEV_SERVER_DEFAULT_HTTP_PORT, @@ -85,12 +83,11 @@ export async function startLWCServer( serverPorts?: { httpPort: number; httpsPort: number }, certData?: SSLCertificateData, workspace?: Workspace, - versionChannelOverride?: VersionChannel, ): Promise { - const channel = OrgUtils.getVersionChannel(connection, versionChannelOverride); - logger.trace(`Using version channel: ${channel}`); + const orgApiVersion = connection.version; + logger.trace(`Starting LWC server for org API version: ${orgApiVersion}`); - const lwcDevServerModule = await loadLwcDevServer(channel); + const lwcDevServerModule = await loadLwcDevServer(orgApiVersion); const config = await createLWCServerConfig(rootDir, token, clientType, serverPorts, certData, workspace); diff --git a/src/shared/dependencyLoader.ts b/src/shared/dependencyLoader.ts index 5288f61a..f604e3bc 100644 --- a/src/shared/dependencyLoader.ts +++ b/src/shared/dependencyLoader.ts @@ -15,7 +15,13 @@ */ import type { Logger } from '@salesforce/core'; -import type { VersionChannel } from './versionResolver.js'; +import packageJsonImport from '../../package.json' with { type: 'json' }; + +type PackageJson = { + apiVersionMetadata: Record; +}; + +const packageJson = packageJsonImport as unknown as PackageJson; /** * Type for dynamically loaded LWC server module @@ -27,40 +33,42 @@ export type LwcDevServerModule = { }; /** - * Loads the LWC dev server module for the specified channel - * Uses dynamic import to load the aliased package at runtime + * Returns a formatted list of all supported API versions + */ +function getSupportedVersionsList(): string { + return Object.keys(packageJson.apiVersionMetadata).sort().join(', '); +} + +/** + * Given an org API version, returns the matched supported version string * - * @param channel - The version channel ('latest', 'prerelease', or 'next') - * @returns The loaded module + * @param orgApiVersion - The API version from the org (e.g., "65.0") + * @returns The matched version string (e.g., "65.0") + * @throws Error if the API version is not supported */ -export async function loadLwcDevServer(channel: VersionChannel): Promise { - const packageName = `@lwc/lwc-dev-server-${channel}`; +function resolveApiVersion(orgApiVersion: string): string { + const metadata = packageJson.apiVersionMetadata; - try { - return (await import(packageName)) as LwcDevServerModule; - } catch (error) { - throw new Error( - `Failed to load LWC dev server for channel '${channel}'. ` + - `Package '${packageName}' could not be imported. ` + - `Error: ${error instanceof Error ? error.message : String(error)}`, - ); + // Exact match + if (metadata[orgApiVersion]) { + return orgApiVersion; } + + throw new Error(`Unsupported org API version: ${orgApiVersion}. This plugin supports: ${getSupportedVersionsList()}`); } /** - * Loads the LWC compiler module for the specified channel - * - * @param channel - The version channel ('latest', 'prerelease', or 'next') - * @returns The loaded compiler module + * Internal helper to dynamically load an aliased dependency */ -export async function loadLwcCompiler(channel: VersionChannel): Promise { - const packageName = `@lwc/sfdc-lwc-compiler-${channel}`; +async function loadDependency(orgApiVersion: string, packagePrefix: string, friendlyName: string): Promise { + const version = resolveApiVersion(orgApiVersion); + const packageName = `${packagePrefix}${version}`; try { - return (await import(packageName)) as unknown; + return (await import(packageName)) as T; } catch (error) { throw new Error( - `Failed to load LWC compiler for channel '${channel}'. ` + + `Failed to load ${friendlyName} for version '${version}'. ` + `Package '${packageName}' could not be imported. ` + `Error: ${error instanceof Error ? error.message : String(error)}`, ); @@ -68,21 +76,32 @@ export async function loadLwcCompiler(channel: VersionChannel): Promise } /** - * Loads the base LWC module for the specified channel + * Loads the LWC dev server module for the specified org API version + * Uses dynamic import to load the aliased package at runtime * - * @param channel - The version channel ('latest', 'prerelease', or 'next') - * @returns The loaded LWC module + * @param orgApiVersion - The API version from the org (e.g., '65.0.1') + * @returns The loaded module */ -export async function loadLwc(channel: VersionChannel): Promise { - const packageName = `lwc-${channel}`; +export async function loadLwcDevServer(orgApiVersion: string): Promise { + return loadDependency(orgApiVersion, '@lwc/lwc-dev-server-', 'LWC dev server'); +} - try { - return (await import(packageName)) as unknown; - } catch (error) { - throw new Error( - `Failed to load LWC for channel '${channel}'. ` + - `Package '${packageName}' could not be imported. ` + - `Error: ${error instanceof Error ? error.message : String(error)}`, - ); - } +/** + * Loads the LWC compiler module for the specified org API version + * + * @param orgApiVersion - The API version from the org (e.g., '65.0.1') + * @returns The loaded compiler module + */ +export async function loadLwcCompiler(orgApiVersion: string): Promise { + return loadDependency(orgApiVersion, '@lwc/sfdc-lwc-compiler-', 'LWC compiler'); +} + +/** + * Loads the base LWC module for the specified org API version + * + * @param orgApiVersion - The API version from the org (e.g., '65.0.1') + * @returns The loaded LWC module + */ +export async function loadLwc(orgApiVersion: string): Promise { + return loadDependency(orgApiVersion, 'lwc-', 'LWC'); } diff --git a/src/shared/orgUtils.ts b/src/shared/orgUtils.ts index cc9928d8..22e00e8c 100644 --- a/src/shared/orgUtils.ts +++ b/src/shared/orgUtils.ts @@ -14,17 +14,7 @@ * limitations under the License. */ -import { Connection, Messages, Logger } from '@salesforce/core'; -import { - VersionChannel, - resolveChannel, - getDefaultChannel, - getAllChannels, - getSupportedVersionsList, -} from './versionResolver.js'; - -Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-lightning-dev', 'shared.utils'); +import { Connection } from '@salesforce/core'; type LightningPreviewMetadataResponse = { enableLightningPreviewPref?: string; @@ -42,8 +32,6 @@ export type AppDefinition = { * the local dev server matches with Org API versions, we rely on defining a metadata section in package.json */ export class OrgUtils { - private static logger = Logger.childFromRoot('OrgUtils'); - /** * Given an app name, it queries the AppDefinition table in the org to find * the DurableId for the app. To do so, it will first attempt at finding the @@ -137,49 +125,4 @@ export class OrgUtils { } throw new Error('Could not save the app server identity token to the org.'); } - - /** - * Determines the version channel for the connected org - * - * @param connection - The connection to the org - * @param overrideChannel - Optional manual override from flag or env var - * @returns The version channel to use for dependencies - * @throws Error if the org version is not supported or invalid override provided - */ - public static getVersionChannel(connection: Connection, overrideChannel?: VersionChannel): VersionChannel { - // Priority 1: Explicit override parameter (from --version-channel flag) - if (overrideChannel) { - return overrideChannel; - } - - // Priority 2: Environment variable override - const envOverride = process.env.FORCE_VERSION_CHANNEL; - if (envOverride) { - const validChannels = getAllChannels(); - if (validChannels.includes(envOverride as VersionChannel)) { - return envOverride as VersionChannel; - } else { - const message = - `Invalid FORCE_VERSION_CHANNEL value: "${envOverride}". ` + `Valid values are: ${validChannels.join(', ')}`; - this.logger.error(message); - throw new Error(message); - } - } - - // Priority 3: Skip check for testing (legacy compatibility) - if (process.env.SKIP_API_VERSION_CHECK === 'true') { - return getDefaultChannel(); - } - - // Priority 4: Automatic detection based on org version - const orgVersion = connection.version; - - try { - return resolveChannel(orgVersion); - } catch (error) { - const message = messages.getMessage('error.org.api-unsupported', [orgVersion, getSupportedVersionsList()]); - this.logger.error(message); - throw new Error(message); - } - } } diff --git a/src/shared/previewUtils.ts b/src/shared/previewUtils.ts index caaec8ce..747acb4b 100644 --- a/src/shared/previewUtils.ts +++ b/src/shared/previewUtils.ts @@ -55,7 +55,7 @@ export class PreviewUtils { public static generateWebSocketUrlForLocalDevServer( platform: string, ports: { httpPort: number; httpsPort: number }, - logger?: Logger + logger?: Logger, ): string { return LwcDevMobileCorePreviewUtils.generateWebSocketUrlForLocalDevServer(platform, ports, logger); } @@ -98,7 +98,7 @@ export class PreviewUtils { public static async getMobileDevice( platform: Platform.ios | Platform.android, deviceId?: string, - logger?: Logger + logger?: Logger, ): Promise { let device: BaseDevice | undefined; @@ -133,7 +133,7 @@ export class PreviewUtils { public static async getLightningExperienceAppId( connection: Connection, appName?: string, - logger?: Logger + logger?: Logger, ): Promise { if (appName) { logger?.debug(`Determining App Id for ${appName}`); @@ -201,7 +201,7 @@ export class PreviewUtils { ldpServerId: string, appId?: string, targetOrg?: string, - auraMode = DevPreviewAuraMode + auraMode = DevPreviewAuraMode, ): string[] { // appPath will resolve to one of the following: // @@ -236,7 +236,7 @@ export class PreviewUtils { ldpServerUrl: string, ldpServerId: string, componentName?: string, - targetOrg?: string + targetOrg?: string, ): string[] { let appPath = `lwr/application/e/devpreview/ai/localdev-preview?ldpServerUrl=${ldpServerUrl}&ldpServerId=${ldpServerId}`; if (componentName) { @@ -267,7 +267,7 @@ export class PreviewUtils { instanceUrl: string, ldpServerUrl: string, ldpServerId: string, - componentName?: string + componentName?: string, ): string { let url = `${instanceUrl}/lwr/application/e/devpreview/ai/localdev-preview?ldpServerUrl=${ldpServerUrl}&ldpServerId=${ldpServerId}`; if (componentName) { @@ -285,7 +285,7 @@ export class PreviewUtils { * @param ldpServerId Record ID for the identity token * @param appName An optional app name for a targeted LEX app * @param appId An optional app id for a targeted LEX app - * @param auraMode An optional Aura Mode (defaults to DEVPREVIEW) + * @param auraMode An Aura Mode (defaults to DEVPREVIEW) * @returns Array of arguments to be used as custom launch arguments when launching a mobile app. */ public static generateMobileAppPreviewLaunchArguments( @@ -293,7 +293,7 @@ export class PreviewUtils { ldpServerId: string, appName?: string, appId?: string, - auraMode = DevPreviewAuraMode + auraMode = DevPreviewAuraMode, ): LaunchArgument[] { const launchArguments: LaunchArgument[] = []; @@ -336,7 +336,7 @@ export class PreviewUtils { * * @param platform A mobile platform (iOS or Android) * @param logger An optional logger to be used for logging - * @param progress An optional spinner indicator for reporting messages + * @param spinner An optional spinner indicator for reporting messages * @param progress An optional progress indicator for reporting progress * @returns The path to downloaded file. */ @@ -344,7 +344,7 @@ export class PreviewUtils { platform: Platform.ios | Platform.android, logger?: Logger, spinner?: Spinner, - progress?: Progress + progress?: Progress, ): Promise { const sfdcUrl = platform === Platform.ios @@ -437,8 +437,6 @@ export class PreviewUtils { return Promise.reject(new Error(sharedMessages.getMessage('error.localdev.not.enabled'))); } - OrgUtils.getVersionChannel(connection); - const appServerIdentity = await PreviewUtils.getOrCreateAppServerIdentity(connection); const ldpServerToken = appServerIdentity.identityToken; const ldpServerId = appServerIdentity.usernameToServerEntityIdMap[username]; diff --git a/src/shared/typeUtils.ts b/src/shared/typeUtils.ts deleted file mode 100644 index 23f8a539..00000000 --- a/src/shared/typeUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -type ValueOf = T[keyof T]; -type Entries = Array<[keyof T, ValueOf]>; - -/** - * Same as `Object.entries()` but with type inference - * - * @param obj the object to get the entries of - * @returns the entries of the object - */ -export function objectEntries(obj: T): Entries { - return Object.entries(obj) as Entries; -} - -/** - * Same as `Object.keys()` but with type inference - * - * @param obj the object to get the keys of - * @returns the keys of the object - */ -export function objectKeys(obj: T): Array { - return Object.keys(obj) as unknown as Array; -} diff --git a/src/shared/versionResolver.ts b/src/shared/versionResolver.ts deleted file mode 100644 index a6888f1b..00000000 --- a/src/shared/versionResolver.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import packageJson from '../../package.json' with { type: 'json' }; -import { objectEntries, objectKeys } from './typeUtils.js'; - -/** - * Resolves org API version to appropriate dependency channel - */ -export type VersionChannel = keyof typeof packageJson.apiVersionMetadata.channels; - -/** - * Extracts major.minor from a version string (e.g., "65.0" from "65.0.1") - */ -function getMajorMinor(version: string): string { - const parts = version.split('.'); - return `${parts[0]}.${parts[1]}`; -} - -/** - * Returns a formatted list of all supported API versions - */ -export function getSupportedVersionsList(): string { - const channels = packageJson.apiVersionMetadata.channels; - const allVersions: string[] = []; - - for (const config of Object.values(channels)) { - allVersions.push(...config.supportedApiVersions); - } - - return allVersions.join(', '); -} - -/** - * Given an org API version, returns the appropriate channel - * - * @param orgApiVersion - The API version from the org (e.g., "65.0") - * @returns The channel to use ('latest' or 'prerelease') - * @throws Error if the API version is not supported by any channel - */ -export function resolveChannel(orgApiVersion: string): VersionChannel { - const channels = packageJson.apiVersionMetadata.channels; - - for (const [channel, config] of objectEntries(channels)) { - if (config.supportedApiVersions.includes(orgApiVersion)) { - return channel; - } - } - - // If no exact match, try to find by major.minor comparison - const orgMajorMinor = getMajorMinor(orgApiVersion); - for (const [channel, config] of objectEntries(channels)) { - for (const supportedVersion of config.supportedApiVersions) { - if (getMajorMinor(supportedVersion) === orgMajorMinor) { - return channel; - } - } - } - - throw new Error(`Unsupported org API version: ${orgApiVersion}. This plugin supports: ${getSupportedVersionsList()}`); -} - -/** - * Returns the default channel from package.json - */ -export function getDefaultChannel(): VersionChannel { - return packageJson.apiVersionMetadata.defaultChannel as VersionChannel; -} - -/** - * Returns a list of all valid version channels - */ -export function getAllChannels(): VersionChannel[] { - return objectKeys(packageJson.apiVersionMetadata.channels); -} diff --git a/src/types/aliased-deps.d.ts b/src/types/aliased-deps.d.ts index b974b074..f9834441 100644 --- a/src/types/aliased-deps.d.ts +++ b/src/types/aliased-deps.d.ts @@ -14,38 +14,38 @@ * limitations under the License. */ -declare module '@lwc/lwc-dev-server-latest' { +declare module '@lwc/lwc-dev-server-65.0' { export * from '@lwc/lwc-dev-server'; } -declare module '@lwc/lwc-dev-server-prerelease' { +declare module '@lwc/lwc-dev-server-66.0' { export * from '@lwc/lwc-dev-server'; } -declare module '@lwc/lwc-dev-server-next' { +declare module '@lwc/lwc-dev-server-67.0' { export * from '@lwc/lwc-dev-server'; } -declare module '@lwc/sfdc-lwc-compiler-latest' { +declare module '@lwc/sfdc-lwc-compiler-65.0' { export * from '@lwc/sfdc-lwc-compiler'; } -declare module '@lwc/sfdc-lwc-compiler-prerelease' { +declare module '@lwc/sfdc-lwc-compiler-66.0' { export * from '@lwc/sfdc-lwc-compiler'; } -declare module '@lwc/sfdc-lwc-compiler-next' { +declare module '@lwc/sfdc-lwc-compiler-67.0' { export * from '@lwc/sfdc-lwc-compiler'; } -declare module 'lwc-latest' { +declare module 'lwc-65.0' { export * from 'lwc'; } -declare module 'lwc-prerelease' { +declare module 'lwc-66.0' { export * from 'lwc'; } -declare module 'lwc-next' { +declare module 'lwc-67.0' { export * from 'lwc'; } diff --git a/test/commands/lightning/dev/app.test.ts b/test/commands/lightning/dev/app.test.ts index cf48a868..a09bd624 100644 --- a/test/commands/lightning/dev/app.test.ts +++ b/test/commands/lightning/dev/app.test.ts @@ -103,7 +103,6 @@ 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, 'getVersionChannel').returns('latest'); MockedLightningPreviewApp = await esmock('../../../../src/commands/lightning/dev/app.js', { '../../../../src/lwc-dev-server/index.js': { diff --git a/test/shared/dependencyLoader.test.ts b/test/shared/dependencyLoader.test.ts index 5211aded..e9961446 100644 --- a/test/shared/dependencyLoader.test.ts +++ b/test/shared/dependencyLoader.test.ts @@ -29,14 +29,14 @@ describe('DependencyLoader', () => { // However, loading LWC modules in Node might still trigger ReferenceErrors if browser globals are missing. // We use a try-catch to handle both cases and just verify the attempt was made. try { - const module = await loadLwcDevServer('latest'); + const module = await loadLwcDevServer('65.0'); expect(module).to.exist; } catch (error) { // If it fails with a ReferenceError or similar, it's still "working" in terms of // attempting to load the right package name. const errorMessage = (error as Error).message; if (errorMessage.includes('could not be imported')) { - expect(errorMessage).to.include('@lwc/lwc-dev-server-latest'); + expect(errorMessage).to.include('@lwc/lwc-dev-server-65.0'); } else { // Other errors (like ReferenceError: Element is not defined) mean the package WAS found and loaded expect(errorMessage).to.not.include('could not be imported'); diff --git a/test/shared/orgUtils.test.ts b/test/shared/orgUtils.test.ts index ae806a91..e08d93f8 100644 --- a/test/shared/orgUtils.test.ts +++ b/test/shared/orgUtils.test.ts @@ -26,57 +26,6 @@ describe('orgUtils', () => { $$.restore(); }); - describe('getVersionChannel', () => { - it('returns override channel if provided', async () => { - const conn = new Connection({ authInfo: new AuthInfo() }); - const channel = OrgUtils.getVersionChannel(conn, 'prerelease'); - expect(channel).to.equal('prerelease'); - }); - - it('returns channel from FORCE_VERSION_CHANNEL env var', async () => { - process.env.FORCE_VERSION_CHANNEL = 'prerelease'; - const conn = new Connection({ authInfo: new AuthInfo() }); - const channel = OrgUtils.getVersionChannel(conn); - expect(channel).to.equal('prerelease'); - - process.env.FORCE_VERSION_CHANNEL = 'next'; - const channelNext = OrgUtils.getVersionChannel(conn); - expect(channelNext).to.equal('next'); - - delete process.env.FORCE_VERSION_CHANNEL; - }); - - it('throws error for invalid FORCE_VERSION_CHANNEL', async () => { - process.env.FORCE_VERSION_CHANNEL = 'invalid'; - const conn = new Connection({ authInfo: new AuthInfo() }); - expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Invalid FORCE_VERSION_CHANNEL/); - delete process.env.FORCE_VERSION_CHANNEL; - }); - - it('returns default channel when SKIP_API_VERSION_CHECK is true', async () => { - process.env.SKIP_API_VERSION_CHECK = 'true'; - const conn = new Connection({ authInfo: new AuthInfo() }); - const channel = OrgUtils.getVersionChannel(conn); - expect(channel).to.equal('latest'); - delete process.env.SKIP_API_VERSION_CHECK; - }); - - it('auto-detects channel based on org version', async () => { - const conn = new Connection({ authInfo: new AuthInfo() }); - $$.SANDBOX.stub(conn, 'version').get(() => '65.0'); - - const channel = OrgUtils.getVersionChannel(conn); - expect(channel).to.equal('latest'); - }); - - it('throws error for unsupported org version', async () => { - const conn = new Connection({ authInfo: new AuthInfo() }); - $$.SANDBOX.stub(conn, 'version').get(() => '64.0'); - - expect(() => OrgUtils.getVersionChannel(conn)).to.throw(/Your org is on API Version 64.0/); - }); - }); - it('getAppDefinitionDurableId returns undefined when no matches found', async () => { $$.SANDBOX.stub(Connection.prototype, 'query').resolves({ records: [], done: true, totalSize: 0 }); const appId = await OrgUtils.getAppDefinitionDurableId(new Connection({ authInfo: new AuthInfo() }), 'blah'); diff --git a/test/shared/previewUtils.test.ts b/test/shared/previewUtils.test.ts index 00ba979f..3f4300e6 100644 --- a/test/shared/previewUtils.test.ts +++ b/test/shared/previewUtils.test.ts @@ -343,7 +343,6 @@ describe('previewUtils', () => { } as Org; $$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true); - $$.SANDBOX.stub(OrgUtils, 'getVersionChannel').returns('latest'); $$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(testIdentityData); const result = await PreviewUtils.initializePreviewConnection(mockOrg); @@ -398,7 +397,6 @@ describe('previewUtils', () => { }; $$.SANDBOX.stub(OrgUtils, 'isLocalDevEnabled').resolves(true); - $$.SANDBOX.stub(OrgUtils, 'getVersionChannel').returns('latest'); $$.SANDBOX.stub(PreviewUtils, 'getOrCreateAppServerIdentity').resolves(identityDataWithoutEntityId); try { diff --git a/test/shared/versionResolver.test.ts b/test/shared/versionResolver.test.ts deleted file mode 100644 index 3e116120..00000000 --- a/test/shared/versionResolver.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { expect } from 'chai'; -import { resolveChannel, getDefaultChannel } from '../../src/shared/versionResolver.js'; - -describe('VersionResolver', () => { - it('resolveChannel returns correct channel for exact match', () => { - expect(resolveChannel('65.0')).to.equal('latest'); - expect(resolveChannel('66.0')).to.equal('prerelease'); - expect(resolveChannel('67.0')).to.equal('next'); - }); - - it('resolveChannel returns correct channel for major.minor match', () => { - expect(resolveChannel('65.0.1')).to.equal('latest'); - expect(resolveChannel('66.0.5')).to.equal('prerelease'); - expect(resolveChannel('67.0.2')).to.equal('next'); - }); - - it('resolveChannel throws error for unsupported version', () => { - expect(() => resolveChannel('64.0')).to.throw(/Unsupported org API version: 64.0/); - }); - - it('getDefaultChannel returns default from package.json', () => { - expect(getDefaultChannel()).to.equal('latest'); - }); -});