Skip to content

Commit 06ee137

Browse files
sf-haulakhclaude
andcommitted
feat: accept 15-char package version IDs in bundle definition file
This change enables the sf package bundle version create command to accept both 15-character and 18-character Salesforce package version IDs (04t IDs) in the bundle definition file. Previously, only 18-character IDs were supported, and providing a 15-character ID would result in an error: "No package version found with alias: 04t5f000000WM9y" Changes: - Added convertTo18CharId() function to convert 15-char IDs to 18-char using the standard Salesforce ID checksum algorithm - Added normalizePackageVersionIds() function to recursively search and normalize all packageVersion fields in the definition JSON - Modified the run() method to read, normalize, and create a temporary definition file when 15-char IDs are detected - Proper cleanup of temporary files in finally block - Added comprehensive unit tests for ID normalization scenarios Also added CLAUDE.md with codebase documentation for future AI instances. Fixes: W-21383062 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent ba27757 commit 06ee137

3 files changed

Lines changed: 390 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is `@salesforce/plugin-packaging`, a Salesforce CLI plugin that provides commands for the Salesforce Packaging Platform. It supports both second-generation managed packages (2GP) and first-generation packages (1GP), as well as unlocked packages and package bundles.
8+
9+
The plugin is built using:
10+
- **oclif** - CLI framework
11+
- **TypeScript** with strict ESM configuration
12+
- **@salesforce/packaging** library (v4.22.8) - Core packaging functionality
13+
- **@salesforce/sf-plugins-core** - Salesforce CLI plugin utilities
14+
15+
## Architecture
16+
17+
### Command Structure
18+
19+
Commands follow the oclif pattern and are organized by topic:
20+
- `src/commands/package/` - Second-generation package commands
21+
- `src/commands/package/version/` - Package version management
22+
- `src/commands/package/bundle/` - Package bundle commands
23+
- `src/commands/package1/` - First-generation package commands
24+
25+
Each command extends `SfCommand<T>` from `@salesforce/sf-plugins-core` and:
26+
- Defines flags using the `Flags` utility (with deprecation support for aliases)
27+
- Uses the `@salesforce/packaging` library for business logic
28+
- Loads messages from `.md` files in the `messages/` directory
29+
- Returns strongly-typed results
30+
31+
### Key Patterns
32+
33+
1. **Hub Flag Pattern**: Most commands require a Dev Hub connection using `requiredHubFlag` from `src/utils/hubFlag.ts`
34+
35+
2. **Message Loading**: Commands load internationalized messages using:
36+
```typescript
37+
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
38+
const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'message_file_name');
39+
```
40+
41+
3. **Packaging Library Integration**: Business logic delegates to the `@salesforce/packaging` library:
42+
```typescript
43+
const result = await Package.create(
44+
flags['target-dev-hub'].getConnection(flags['api-version']),
45+
this.project!,
46+
options
47+
);
48+
```
49+
50+
4. **JSON Schemas**: Each command has a corresponding JSON schema in `schemas/` for output validation
51+
52+
## Development Commands
53+
54+
### Setup
55+
```bash
56+
yarn install # Install dependencies
57+
yarn build # Compile TypeScript and run linting
58+
yarn clean-all # Clean everything including node_modules
59+
```
60+
61+
### Building
62+
```bash
63+
yarn compile # Compile TypeScript only
64+
yarn lint # Run ESLint
65+
yarn build # Compile + lint
66+
```
67+
68+
### Testing
69+
```bash
70+
yarn test # Run unit tests (mocha)
71+
yarn test:nuts # Run NUTs (non-unit tests) - requires Dev Hub
72+
yarn test:nuts:package # Run package-specific NUTs
73+
yarn test:nuts:package1 # Run 1GP package NUTs
74+
```
75+
76+
**Important**: For 1GP NUTs, set `ONEGP_TESTKIT_AUTH_URL` environment variable for target org authentication.
77+
78+
### Running Commands Locally
79+
```bash
80+
# Direct execution (must compile first)
81+
./bin/run.js package:create --help
82+
83+
# Link to sf CLI
84+
sf plugins:link .
85+
sf package create --help
86+
```
87+
88+
### Formatting
89+
```bash
90+
yarn format # Format code with prettier
91+
```
92+
93+
## Testing Strategy
94+
95+
### Unit Tests
96+
- Located in `test/commands/` mirroring `src/commands/` structure
97+
- Named with `.test.ts` suffix
98+
- Run with mocha using ts-node
99+
- Use stubs/mocks for external dependencies
100+
- Aim for 95%+ code coverage
101+
102+
### NUTs (Non-Unit Tests)
103+
- Named with `.nut.ts` suffix
104+
- Use `@salesforce/cli-plugins-testkit` framework
105+
- Run against real Salesforce orgs (require authenticated Dev Hub)
106+
- Test end-to-end command functionality
107+
- Run in parallel with `--jobs 20` flag
108+
109+
## Linking the Packaging Library
110+
111+
When developing against a local version of `@salesforce/packaging`:
112+
113+
1. In the packaging library: `yarn link`
114+
2. In this plugin: `yarn link "@salesforce/packaging"`
115+
3. Make changes in packaging library and rebuild
116+
4. Test in this plugin
117+
118+
See [packaging library docs](https://github.com/forcedotcom/packaging/blob/main/DEVELOPING.md#linking-to-the-packaging-plugin) for details.
119+
120+
## Commit and Release Process
121+
122+
### Commit Messages
123+
Follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/):
124+
- `fix:` - Patch version bump
125+
- `feat:` - Minor version bump
126+
- Breaking changes require coordination with CLI team for major version bump
127+
128+
Commit messages are enforced via commitlint and husky pre-commit hooks.
129+
130+
### Release Process
131+
- PRs merged to `main` automatically publish to npmjs
132+
- Version bump is determined by commit message prefixes
133+
- README is auto-updated via `yarn version` script (runs `oclif readme`)
134+
135+
## Key Configuration Files
136+
137+
- `package.json` - Dependencies, scripts, oclif configuration
138+
- `tsconfig.json` - Extends `@salesforce/dev-config/tsconfig-strict-esm`
139+
- `.mocharc.json` - Mocha test configuration
140+
- `.nycrc` - Code coverage configuration
141+
- `wireit` config in `package.json` - Build orchestration and caching
142+
143+
## Command Development Checklist
144+
145+
When adding a new command:
146+
147+
1. Create command file in appropriate `src/commands/` subdirectory
148+
2. Create message file in `messages/` (name matches command path with underscores)
149+
3. Add JSON schema in `schemas/` for output validation
150+
4. Write unit tests in `test/commands/` mirroring source structure
151+
5. Write NUTs if command interacts with Salesforce APIs
152+
6. Update command snapshot: `./bin/dev.js snapshot:compare`
153+
7. Regenerate README: `yarn version`
154+
155+
## Important Notes
156+
157+
- All package commands require a Dev Hub org (`--target-dev-hub` or `target-dev-hub` config)
158+
- Package version creation is async - commands return a request ID to poll for status
159+
- Installation keys can be required or bypassed (never both)
160+
- Second-generation packages require project context (`requiresProject: true`)
161+
- ESM modules are used throughout (`"type": "module"` in package.json)

src/commands/package/bundle/version/create.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,78 @@ import {
2424
import { Messages, Lifecycle } from '@salesforce/core';
2525
import { camelCaseToTitleCase, Duration } from '@salesforce/kit';
2626
import { requiredHubFlag } from '../../../../utils/hubFlag.js';
27+
import fs from 'node:fs';
28+
import path from 'node:path';
29+
import os from 'node:os';
2730

2831
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2932
// TODO: Update messages
3033
const messages = Messages.loadMessages('@salesforce/plugin-packaging', 'bundle_version_create');
3134
export type BundleVersionCreate = BundleSObjects.PackageBundleVersionCreateRequestResult;
3235

36+
/**
37+
* Converts a 15-character Salesforce ID to its 18-character equivalent.
38+
* If the ID is already 18 characters or not a valid Salesforce ID format, returns it unchanged.
39+
*
40+
* @param id - The Salesforce ID to convert
41+
* @returns The 18-character Salesforce ID
42+
*/
43+
function convertTo18CharId(id: string): string {
44+
// If already 18 chars or not 15 chars, return as-is
45+
if (!id || id.length !== 15) {
46+
return id;
47+
}
48+
49+
// Salesforce ID conversion algorithm
50+
// For each chunk of 5 characters, calculate a checksum character
51+
const suffix: string[] = [];
52+
53+
for (let i = 0; i < 3; i++) {
54+
let flags = 0;
55+
for (let j = 0; j < 5; j++) {
56+
const char = id.charAt(i * 5 + j);
57+
// Check if character is uppercase (A-Z have higher ASCII values than lowercase)
58+
if (char >= 'A' && char <= 'Z') {
59+
flags += 1 << j;
60+
}
61+
}
62+
// Convert flags to a base-32 character
63+
suffix.push('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'.charAt(flags));
64+
}
65+
66+
return id + suffix.join('');
67+
}
68+
69+
/**
70+
* Normalizes package version IDs in a bundle definition object.
71+
* Converts any 15-character package version IDs to 18-character format.
72+
*
73+
* @param obj - The object to normalize (can be nested)
74+
* @returns The normalized object
75+
*/
76+
function normalizePackageVersionIds(obj: unknown): unknown {
77+
if (typeof obj !== 'object' || obj === null) {
78+
return obj;
79+
}
80+
81+
if (Array.isArray(obj)) {
82+
return obj.map((item) => normalizePackageVersionIds(item));
83+
}
84+
85+
const result: Record<string, unknown> = {};
86+
for (const [key, value] of Object.entries(obj)) {
87+
if (key === 'packageVersion' && typeof value === 'string' && value.startsWith('04t')) {
88+
// Normalize package version IDs (04t prefix)
89+
result[key] = convertTo18CharId(value);
90+
} else if (typeof value === 'object') {
91+
result[key] = normalizePackageVersionIds(value);
92+
} else {
93+
result[key] = value;
94+
}
95+
}
96+
return result;
97+
}
98+
3399
export class PackageBundlesCreate extends SfCommand<BundleSObjects.PackageBundleVersionCreateRequestResult> {
34100
public static readonly hidden = true;
35101
public static state = 'beta';
@@ -81,11 +147,38 @@ export class PackageBundlesCreate extends SfCommand<BundleSObjects.PackageBundle
81147
minorVersion = versionParts[1] || '';
82148
}
83149

150+
// Read and normalize the definition file to handle 15-char package version IDs
151+
let definitionFilePath = flags['definition-file'];
152+
let tempFilePath: string | undefined;
153+
154+
try {
155+
// Read the definition file
156+
const definitionContent = await fs.promises.readFile(definitionFilePath, 'utf8');
157+
const definitionJson = JSON.parse(definitionContent) as unknown;
158+
159+
// Normalize any 15-character package version IDs to 18-character format
160+
const normalizedJson = normalizePackageVersionIds(definitionJson);
161+
162+
// Check if any normalization occurred by comparing stringified versions
163+
if (JSON.stringify(definitionJson) !== JSON.stringify(normalizedJson)) {
164+
// Create a temporary file with normalized content
165+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'sf-bundle-'));
166+
tempFilePath = path.join(tempDir, 'normalized-definition.json');
167+
await fs.promises.writeFile(tempFilePath, JSON.stringify(normalizedJson, null, 2), 'utf8');
168+
definitionFilePath = tempFilePath;
169+
this.debug(`Normalized package version IDs in definition file. Using temporary file: ${tempFilePath}`);
170+
}
171+
} catch (error) {
172+
// If reading/parsing fails, let the packaging library handle the error
173+
// This preserves the original error messages for invalid JSON, missing files, etc.
174+
this.debug(`Could not normalize definition file: ${error instanceof Error ? error.message : String(error)}`);
175+
}
176+
84177
const options: BundleVersionCreateOptions = {
85178
connection: flags['target-dev-hub'].getConnection(flags['api-version']),
86179
project: this.project!,
87180
PackageBundle: flags.bundle,
88-
BundleVersionComponentsPath: flags['definition-file'],
181+
BundleVersionComponentsPath: definitionFilePath,
89182
Description: flags.description,
90183
MajorVersion: majorVersion,
91184
MinorVersion: minorVersion,
@@ -134,6 +227,20 @@ export class PackageBundlesCreate extends SfCommand<BundleSObjects.PackageBundle
134227
this.spinner.stop();
135228
}
136229
throw error;
230+
} finally {
231+
// Clean up temporary file if it was created
232+
if (tempFilePath) {
233+
try {
234+
const tempDir = path.dirname(tempFilePath);
235+
await fs.promises.rm(tempDir, { recursive: true, force: true });
236+
this.debug(`Cleaned up temporary definition file: ${tempFilePath}`);
237+
} catch (cleanupError) {
238+
// Log but don't fail if cleanup fails
239+
this.debug(
240+
`Failed to clean up temporary file: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`
241+
);
242+
}
243+
}
137244
}
138245

139246
// Stop spinner only if it was started - stop it cleanly without a message

0 commit comments

Comments
 (0)