├── .prettierrc.json ├── .commitlintrc.json ├── messages ├── bundle_version.md ├── bundle_create.md ├── package_create.md ├── package_uninstall.md ├── bundle_utils.md ├── bundle_install.md ├── profile_api.md ├── version_number.md ├── package_ancestry.md ├── package1Version.md ├── subscriber_package_version.md ├── package_install.md ├── package_version_dependency.md ├── bundle_version_create.md ├── package_version.md ├── pkg_utils.md ├── package.md └── package_version_create.md ├── .husky ├── commit-msg ├── pre-push └── pre-commit ├── test ├── data │ ├── package-1gp.zip │ └── package-2gp.zip ├── package │ ├── resources │ │ └── packageProject │ │ │ ├── force-app │ │ │ └── main │ │ │ │ └── default │ │ │ │ ├── classes │ │ │ │ ├── MyApexClass.cls │ │ │ │ ├── MyApexClass.cls-meta.xml │ │ │ │ ├── MyApexClass_Test.cls-meta.xml │ │ │ │ └── MyApexClass_Test.cls │ │ │ │ ├── aura │ │ │ │ └── .eslintrc.json │ │ │ │ └── lwc │ │ │ │ └── .eslintrc.json │ │ │ ├── .prettierignore │ │ │ ├── sfdx-project.json │ │ │ ├── scripts │ │ │ ├── soql │ │ │ │ └── account.soql │ │ │ └── apex │ │ │ │ └── hello.apex │ │ │ ├── .prettierrc │ │ │ ├── .eslintignore │ │ │ ├── .forceignore │ │ │ ├── config │ │ │ └── project-scratch-def.json │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ └── package.json │ ├── bundleList.test.ts │ ├── versionNumber.test.ts │ ├── bundleVersionList.test.ts │ ├── packageVersion.test.ts │ ├── uninstallPackage.test.ts │ ├── profileRewriter.test.ts │ ├── bundleCreate.test.ts │ ├── packageCreate.test.ts │ ├── packageVersionList.test.ts │ └── ancestry.nut.ts ├── tsconfig.json ├── init.js ├── .eslintrc.cjs └── package1 │ ├── packageDisplay.test.ts │ ├── package1VersionList.test.ts │ └── package1VersionCreate.test.ts ├── typedoc.json ├── .nycrc ├── .git2gus └── config.json ├── .mocharc.json ├── .github ├── workflows │ ├── automerge.yml │ ├── devScripts.yml │ ├── validate-pr.yml │ ├── onRelease.yml │ ├── notify-slack-on-pr-open.yml │ ├── create-github-release.yml │ ├── test.yml │ └── failureNotifications.yml └── dependabot.yml ├── CODEOWNERS ├── .eslintrc.cjs ├── SECURITY.md ├── tsconfig.json ├── .vscode └── launch.json ├── src ├── package1 │ ├── index.ts │ └── package1Version.ts ├── interfaces │ ├── index.ts │ ├── bundleInterfacesAndType.ts │ └── bundleSObjects.ts ├── utils │ ├── index.ts │ └── bundleUtils.ts ├── exported.ts └── package │ ├── index.ts │ ├── packageVersionCreateRequestReport.ts │ ├── packageDelete.ts │ ├── packageBundleCreate.ts │ ├── packageBundle.ts │ ├── packageCreate.ts │ ├── packageUninstall.ts │ ├── versionNumber.ts │ ├── packageVersionReport.ts │ ├── packageVersionList.ts │ ├── packageProfileApi.ts │ ├── packageVersionCreateRequest.ts │ └── profileRewriter.ts ├── .gitignore ├── scripts └── repl.js ├── README.md ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md └── DEVELOPING.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@salesforce/prettier-config" 2 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /messages/bundle_version.md: -------------------------------------------------------------------------------- 1 | # componentRecordMissing 2 | 3 | Component record is missing 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn build && yarn test 5 | -------------------------------------------------------------------------------- /messages/bundle_create.md: -------------------------------------------------------------------------------- 1 | # failedToCreatePackageBundle 2 | 3 | Failed to create package bundle 4 | -------------------------------------------------------------------------------- /messages/package_create.md: -------------------------------------------------------------------------------- 1 | # unableToFindPackageWithId 2 | 3 | Unable to find Package with Id: "%s" 4 | -------------------------------------------------------------------------------- /test/data/package-1gp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/packaging/HEAD/test/data/package-1gp.zip -------------------------------------------------------------------------------- /test/data/package-2gp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/forcedotcom/packaging/HEAD/test/data/package-2gp.zip -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint && yarn pretty-quick --staged 5 | -------------------------------------------------------------------------------- /messages/package_uninstall.md: -------------------------------------------------------------------------------- 1 | # uninstallErrorAction 2 | 3 | Verify installed package ID and resolve errors, then try again. 4 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/classes/MyApexClass.cls: -------------------------------------------------------------------------------- 1 | public with sharing class MyApexClass { } 2 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "default", 3 | "excludePrivate": true, 4 | "excludeProtected": true, 5 | "entryPoints": ["src"], 6 | "entryPointStrategy": "expand" 7 | } 8 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "nyc": { 3 | "extends": "@salesforce/dev-config/nyc", 4 | "lines": 87, 5 | "statements": 87, 6 | "functions": 89, 7 | "branches": 74, 8 | "exclude": ["**/*.d.ts", "test/**/*.ts"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.git2gus/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "productTag": "a1aB00000004Bx8IAE", 3 | "defaultBuild": "offcore.tooling.58", 4 | "issueTypeLabels": { 5 | "feature": "USER STORY", 6 | "regression": "BUG P1", 7 | "bug": "BUG P3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": "test/init.js, ts-node/register, source-map-support/register", 3 | "watch-extensions": "ts", 4 | "watch-files": ["src", "test"], 5 | "recursive": true, 6 | "reporter": "spec", 7 | "timeout": 20000 8 | } 9 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '42 2,5,8,11 * * *' 6 | 7 | jobs: 8 | automerge: 9 | uses: salesforcecli/github-workflows/.github/workflows/automerge.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/devScripts.yml: -------------------------------------------------------------------------------- 1 | name: devScripts 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '50 6 * * 0' 6 | 7 | jobs: 8 | update: 9 | uses: salesforcecli/github-workflows/.github/workflows/devScriptsUpdate.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/classes/MyApexClass.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/classes/MyApexClass_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 51.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/.prettierignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running prettier 2 | # More information: https://prettier.io/docs/en/ignore.html 3 | # 4 | 5 | **/staticresources/** 6 | .localdevserver 7 | .sfdx 8 | .vscode 9 | 10 | coverage/ -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Technical writers will be added as reviewers on markdown changes. 2 | *.md @forcedotcom/cli-docs 3 | 4 | # Comment line immediately above ownership line is reserved for related other information. Please be careful while editing. 5 | #ECCN:Open Source 6 | #GUSINFO:Platform CLI,Platform CLI -------------------------------------------------------------------------------- /messages/bundle_utils.md: -------------------------------------------------------------------------------- 1 | # STRING_TOO_LONG 2 | 3 | Either name or description has exceeded the 255 charector limit. 4 | 5 | # invalidIdOrAlias 6 | 7 | The %s : %s isn't defined in the sfdx-project.json. Add it to the packageBundles section and add the alias to packageBundleAliases with its %s ID. 8 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "namespace": "", 9 | "sfdcLoginUrl": "https://login.salesforce.com", 10 | "sourceApiVersion": "51.0" 11 | } 12 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/scripts/soql/account.soql: -------------------------------------------------------------------------------- 1 | // Use .soql files to store SOQL queries. 2 | // You can execute queries in VS Code by selecting the 3 | // query text and running the command: 4 | // SFDX: Execute SOQL Query with Currently Selected Text 5 | 6 | SELECT Id, Name FROM Account 7 | -------------------------------------------------------------------------------- /messages/bundle_install.md: -------------------------------------------------------------------------------- 1 | # failedToGetPackageBundleInstallStatus 2 | 3 | Failed to get package bundle install status 4 | 5 | # failedToInstallPackageBundle 6 | 7 | Failed to install package bundle 8 | 9 | # noPackageBundleVersionFoundWithAlias 10 | 11 | No package bundle version found with alias: %s 12 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Generated - Do not modify. Controlled by @salesforce/dev-scripts 2 | // See more at https://github.com/forcedotcom/sfdx-dev-packages/tree/master/packages/dev-scripts 3 | 4 | module.exports = { 5 | extends: ['eslint-config-salesforce-typescript', 'eslint-config-salesforce-license', 'plugin:sf-plugin/library'], 6 | }; 7 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "overrides": [ 4 | { 5 | "files": "**/lwc/**/*.html", 6 | "options": { "parser": "lwc" } 7 | }, 8 | { 9 | "files": "*.{cmp,page,component}", 10 | "options": { "parser": "html" } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr.yml: -------------------------------------------------------------------------------- 1 | name: pr-validation 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened, edited] 6 | # only applies to PRs that want to merge to main 7 | branches: [main] 8 | 9 | jobs: 10 | pr-validation: 11 | uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main 12 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/aura/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@salesforce/eslint-plugin-aura"], 3 | "extends": ["plugin:@salesforce/eslint-plugin-aura/recommended", "prettier"], 4 | "rules": { 5 | "func-names": "off", 6 | "vars-on-top": "off", 7 | "no-unused-expressions": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/lwc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@salesforce/eslint-config-lwc/recommended", "prettier"], 3 | "overrides": [ 4 | { 5 | "files": ["*.test.js"], 6 | "rules": { 7 | "@lwc/lwc/no-unexpected-wire-adapter-usages": "off" 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/.eslintignore: -------------------------------------------------------------------------------- 1 | **/lwc/**/*.css 2 | **/lwc/**/*.html 3 | **/lwc/**/*.json 4 | **/lwc/**/*.svg 5 | **/lwc/**/*.xml 6 | **/aura/**/*.auradoc 7 | **/aura/**/*.cmp 8 | **/aura/**/*.css 9 | **/aura/**/*.design 10 | **/aura/**/*.evt 11 | **/aura/**/*.json 12 | **/aura/**/*.svg 13 | **/aura/**/*.tokens 14 | **/aura/**/*.xml 15 | .sfdx -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-test-strict", 3 | "compilerOptions": { 4 | "baseUrl": "..", 5 | "resolveJsonModule": true, 6 | "paths": { 7 | "@salesforce/kit": ["node_modules/@salesforce/kit"] 8 | }, 9 | "module": "Node16", 10 | "moduleResolution": "Node16" 11 | }, 12 | "include": ["./**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/force-app/main/default/classes/MyApexClass_Test.cls: -------------------------------------------------------------------------------- 1 | @isTest 2 | public class MyApexClass_Test { 3 | public static final Boolean FAIL = false; 4 | 5 | @isTest static void passingTest() { 6 | System.assertEquals(1, 1); 7 | } 8 | 9 | @isTest static void failingTest() { 10 | System.assertEquals(1, FAIL ? 2 : 1); 11 | } 12 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/.forceignore: -------------------------------------------------------------------------------- 1 | # List files or directories below to ignore them when running force:source:push, force:source:pull, and force:source:status 2 | # More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm 3 | # 4 | 5 | package.xml 6 | 7 | # LWC configuration files 8 | **/jsconfig.json 9 | **/.eslintrc.json 10 | 11 | # LWC Jest 12 | **/__tests__/** -------------------------------------------------------------------------------- /test/package/resources/packageProject/config/project-scratch-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "orgName": "packaging integration target", 3 | "edition": "Developer", 4 | "adminEmail": "test@mailinator.com", 5 | "features": ["MultiCurrency"], 6 | "settings": { 7 | "lightningExperienceSettings": { 8 | "enableS1DesktopEnabled": true 9 | }, 10 | "languageSettings": { 11 | "enableTranslationWorkbench": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-strict", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "resolveJsonModule": true, 6 | "rootDir": "./src", 7 | "skipLibCheck": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@salesforce/kit": ["node_modules/@salesforce/kit"] 11 | }, 12 | "module": "Node16", 13 | "moduleResolution": "Node16" 14 | }, 15 | "include": ["./src/**/*.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229, 12 | "skipFiles": ["/**"] 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/scripts/apex/hello.apex: -------------------------------------------------------------------------------- 1 | // Use .apex files to store anonymous Apex. 2 | // You can execute anonymous Apex in VS Code by selecting the 3 | // apex text and running the command: 4 | // SFDX: Execute Anonymous Apex with Currently Selected Text 5 | // You can also execute the entire file by running the command: 6 | // SFDX: Execute Anonymous Apex with Editor Contents 7 | 8 | string tempvar = 'Enter_your_name_here'; 9 | System.debug('Hello World!'); 10 | System.debug('My name is ' + tempvar); -------------------------------------------------------------------------------- /messages/profile_api.md: -------------------------------------------------------------------------------- 1 | # addProfileToPackage 2 | 3 | The profile "%s" from the "%s" directory was added to this package version. 4 | 5 | # removeProfileSetting 6 | 7 | The "%s" profile setting was removed from the "%s" profile. 8 | 9 | # removeProfile 10 | 11 | The "%s" profile has been removed from the package because its settings don't correspond with anything in the package version. 12 | 13 | # profileNotIncluded 14 | 15 | The "%s" profile was not added to the package because its settings don't correspond to anything in the package version." 16 | -------------------------------------------------------------------------------- /messages/version_number.md: -------------------------------------------------------------------------------- 1 | # errorInvalidBuildNumberToken 2 | 3 | The provided VersionNumber '%s' is invalid. Build number token must be a number or one of these tokens '%s'. 4 | 5 | # errorMissingVersionNumber 6 | 7 | The VersionNumber property must be specified. 8 | 9 | # errorInvalidMajorMinorPatchNumber 10 | 11 | VersionNumber parts major, minor or patch must be a number but the value found is [%s]. 12 | 13 | # errorInvalidVersionNumber 14 | 15 | VersionNumber must be in the format major.minor.patch.build but the value found is [%s]. 16 | 17 | # invalidMajorMinorFormat 18 | 19 | Version supplied, %s, is not formatted correctly. Enter in major.minor format, for example, 3.2. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'saturday' 8 | versioning-strategy: 'increase' 9 | labels: 10 | - 'dependencies' 11 | open-pull-requests-limit: 5 12 | pull-request-branch-name: 13 | separator: '-' 14 | commit-message: 15 | # cause a release for non-dev-deps 16 | prefix: fix(deps) 17 | # no release for dev-deps 18 | prefix-development: chore(dev-deps) 19 | ignore: 20 | - dependency-name: '@salesforce/dev-scripts' 21 | - dependency-name: '*' 22 | update-types: ['version-update:semver-major'] 23 | -------------------------------------------------------------------------------- /src/package1/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export { Package1Version } from './package1Version'; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- CLEAN 2 | 3 | 4 | 5 | # use yarn by default, so ignore npm 6 | package-lock.json 7 | 8 | # never checkin npm config 9 | .npmrc 10 | 11 | # debug logs 12 | npm-error.log 13 | yarn-error.log 14 | lerna-debug.log 15 | 16 | # compile source 17 | lib 18 | 19 | # test artifacts 20 | *xunit.xml 21 | *checkstyle.xml 22 | *unitcoverage 23 | .nyc_output 24 | coverage 25 | test_session** 26 | 27 | # generated docs 28 | docs 29 | 30 | # history for repl script 31 | .repl_history 32 | 33 | # -- CLEAN ALL 34 | *.tsbuildinfo 35 | node_modules 36 | 37 | 38 | .eslintcache 39 | .wireit 40 | 41 | # -- 42 | # put files here you don't want cleaned with sf-clean 43 | 44 | # os specific files 45 | .DS_Store 46 | .idea 47 | 48 | .sfdx 49 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | const path = require('path'); 17 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json'); 18 | -------------------------------------------------------------------------------- /messages/package_ancestry.md: -------------------------------------------------------------------------------- 1 | # idOrAliasNotFound 2 | 3 | Can't find package with id or alias: %s 4 | 5 | # unlockedPackageError 6 | 7 | Can’t display package ancestry. Package ancestry is available only for second-generation managed packages. Retry this command and specify a second-generation managed package or package version. 8 | 9 | # noVersionsError 10 | 11 | Can’t display package ancestry. The specified package has no associated released package versions. Retry this command after you create and promote at least one package version. 12 | 13 | # versionNotFound 14 | 15 | Can’t display the ancestry tree for %s. Verify the package version number (starts with 04t) or the package version alias listed in the sfdx-project.json file, and try creating the ancestry tree again. 16 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export * from './packagingInterfacesAndType'; 17 | export * from './packagingSObjects'; 18 | export * from './bundleInterfacesAndType'; 19 | export * from './bundleSObjects'; 20 | -------------------------------------------------------------------------------- /messages/package1Version.md: -------------------------------------------------------------------------------- 1 | # package1VersionCreateCommandUploadFailureDefault 2 | 3 | Package version creation failed with unknown error 4 | 5 | # package1VersionCreateCommandUploadFailure 6 | 7 | Package upload failed. 8 | %s 9 | 10 | # invalid04tId 11 | 12 | Specify a valid package version ID (starts with 04t), received %s 13 | 14 | # invalid0HDId 15 | 16 | Specify a valid Package Upload Request ID (starts with 0HD), received %s 17 | 18 | # invalid033Id 19 | 20 | Specify a valid package metadata package ID (starts with 033), received %s 21 | 22 | # missingMetadataPackageId 23 | 24 | A MetadataPackageId was not provided, but is required, it must start with 033 25 | 26 | # missingVersionName 27 | 28 | a VersionName was not provided, but is required 29 | 30 | # createFailed 31 | 32 | The attempt to create a package version failed. See the following for more information: %s 33 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export { 17 | INSTALL_URL_BASE, 18 | getContainerOptions, 19 | getPackageVersionStrings, 20 | getPackageVersionNumber, 21 | } from './packageUtils'; 22 | 23 | export { massageErrorMessage } from './bundleUtils'; 24 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used for Git repositories to specify intentionally untracked files that Git should ignore. 2 | # If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore 3 | # For useful gitignore templates see: https://github.com/github/gitignore 4 | 5 | # Salesforce cache 6 | .sfdx/ 7 | .localdevserver/ 8 | 9 | # LWC VSCode autocomplete 10 | **/lwc/jsconfig.json 11 | 12 | # LWC Jest coverage reports 13 | coverage/ 14 | 15 | # SOQL Query Results 16 | **/scripts/soql/query-results 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Dependency directories 26 | node_modules/ 27 | 28 | # Eslint cache 29 | .eslintcache 30 | 31 | # MacOS system files 32 | .DS_Store 33 | 34 | # Windows system files 35 | Thumbs.db 36 | ehthumbs.db 37 | [Dd]esktop.ini 38 | $RECYCLE.BIN/ 39 | -------------------------------------------------------------------------------- /messages/subscriber_package_version.md: -------------------------------------------------------------------------------- 1 | # errorInvalidIdNoRecordFound 2 | 3 | The subscriber package version %s is either invalid or not yet available on your instance of salesforce.com. First double-check the ID to ensure it's correct. If it is, check back after a while and retry the package install. 4 | 5 | # errorInvalidAliasOrId 6 | 7 | Invalid alias or ID: %s. Either your alias is invalid or undefined, or the ID (04t) provided is invalid. 8 | 9 | # packageVersionInstallRequestIdInvalid 10 | 11 | The provided package install request ID: [%s] is invalid. It must be a 15 or 18 character package install request ID (0Hf). 12 | 13 | # packageVersionUninstallRequestIdInvalid 14 | 15 | The provided package uninstall request ID: [%s] is invalid. It must be a 15 or 18 character package install request ID (06y). 16 | 17 | # packageVersionInstallRequestNotFound 18 | 19 | The provided package install request ID: [%s] could not be found. 20 | -------------------------------------------------------------------------------- /test/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.cjs', 3 | // Allow describe and it 4 | env: { mocha: true }, 5 | rules: { 6 | '@typescript-eslint/no-misused-promises': 'off', 7 | // Allow assert style expressions. i.e. expect(true).to.be.true 8 | 'no-unused-expressions': 'off', 9 | 10 | // It is common for tests to stub out method. 11 | '@typescript-eslint/ban-ts-comment': 'off', 12 | '@typescript-eslint/no-unsafe-member-access': 'off', 13 | // Return types are defined by the source code. Allows for quick overwrites. 14 | '@typescript-eslint/explicit-function-return-type': 'off', 15 | // Mocked out the methods that shouldn't do anything in the tests. 16 | '@typescript-eslint/no-empty-function': 'off', 17 | // Easily return a promise in a mocked method. 18 | '@typescript-eslint/require-await': 'off', 19 | }, 20 | ignorePatterns: ['package/resources/**/*'], 21 | }; 22 | -------------------------------------------------------------------------------- /src/exported.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export * from './interfaces'; 17 | export * from './package'; 18 | export * from './package1'; 19 | export * from './utils'; 20 | export * from './package/packageBundle'; 21 | export * from './package/packageBundleVersionCreate'; 22 | export * from './package/packageBundleInstall'; 23 | -------------------------------------------------------------------------------- /messages/package_install.md: -------------------------------------------------------------------------------- 1 | # upgradeTypeOnlyForUnlockedWarning 2 | 3 | WARNING: We ignored the upgradetype parameter when installing this package version. The upgradetype parameter is available only for unlocked package installations. Managed package upgrades always default to using the Mixed upgrade type. 4 | 5 | # apexCompileOnlyForUnlockedWarning 6 | 7 | WARNING: We ignored the apexcompile parameter when installing this package version. The apexcompile parameter is available only for installations of unlocked packages. 8 | 9 | # packageInstallPolling 10 | 11 | Waiting for the package install request to complete. Status = %s 12 | 13 | # packageInstallRequestError 14 | 15 | Failed to create PackageInstallRequest for: %s 16 | Due to: %s 17 | 18 | # publishWaitProgress 19 | 20 | Waiting for the Subscriber Package Version ID to be published to the target org.%s 21 | 22 | # subscriberPackageVersionNotPublished 23 | 24 | The subscriber package version is not fully available. 25 | -------------------------------------------------------------------------------- /messages/package_version_dependency.md: -------------------------------------------------------------------------------- 1 | # invalidPackageVersionIdError 2 | 3 | Can't display package dependencies. The package version ID %s you specified is invalid. Review the package version ID and then retry this command. 4 | 5 | # invalidDependencyGraphError 6 | 7 | Can't display package dependencies. There's an issue generating the dependency graph. Before retrying this command, make sure you added the calculateTransitiveDependencies parameter to the sfdx-project.json file and set the value to "true". After setting the attribute and before retrying this command, you must create a new package version. 8 | 9 | # noDependencyGraphJsonMustProvideVersion 10 | 11 | Can't display package dependencies. This Package2VersionCreateRequest does not have CalcTransitiveDependencies set to true. To display package dependencies, either specify a package version id (starts with 04t or 05i) or create a new package version with the calculateTransitiveDependencies parameter in the sfdx-project.json file to "true". 12 | -------------------------------------------------------------------------------- /src/package/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | export * from './package'; 17 | export * from './packageVersion'; 18 | export * from './subscriberPackageVersion'; 19 | export * from './packagePushUpgrade'; 20 | export { VersionNumber } from './versionNumber'; 21 | export * from './packageBundle'; 22 | export * from './packageBundleVersion'; 23 | export * from './packageBundleInstall'; 24 | -------------------------------------------------------------------------------- /src/utils/bundleUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Messages } from '@salesforce/core'; 18 | 19 | Messages.importMessagesDirectory(__dirname); 20 | const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_utils'); 21 | 22 | export function massageErrorMessage(err: Error): Error { 23 | if (err.name === 'STRING_TOO_LONG') { 24 | err['message'] = messages.getMessage('STRING_TOO_LONG'); 25 | } 26 | 27 | return err; 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | # support manual release in case something goes wrong and needs to be repeated or tested 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: tag that needs to publish 11 | type: string 12 | required: true 13 | jobs: 14 | # parses the package.json version and detects prerelease tag (ex: beta from 4.4.4-beta.0) 15 | getDistTag: 16 | outputs: 17 | tag: ${{ steps.distTag.outputs.tag }} 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | ref: ${{ github.event.release.tag_name || inputs.tag }} 23 | - uses: salesforcecli/github-workflows/.github/actions/getPreReleaseTag@main 24 | id: distTag 25 | npm: 26 | uses: salesforcecli/github-workflows/.github/workflows/npmPublish.yml@main 27 | needs: [getDistTag] 28 | with: 29 | tag: ${{ needs.getDistTag.outputs.tag || 'latest' }} 30 | githubTag: ${{ github.event.release.tag_name || inputs.tag }} 31 | secrets: inherit 32 | -------------------------------------------------------------------------------- /scripts/repl.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const repl = require('repl'); 4 | const { Org, SfProject } = require('@salesforce/core'); 5 | const { Package, PackageVersion, SubscriberPackageVersion, Package1Version } = require('../lib/exported'); 6 | 7 | const startMessage = ` 8 | Usage: 9 | // Get an org Connection 10 | const conn = await getConnection(username); 11 | 12 | // Get an SfProject 13 | const project = SfProject.getInstance(projectPath); 14 | 15 | // Use the Connection and SfProject when calling methods of: 16 | * Package 17 | * PackageVersion 18 | * SubscriberPackageVersion 19 | * Package1Version 20 | `; 21 | console.log(startMessage); 22 | 23 | const replServer = repl.start({ breakEvalOnSigint: true }); 24 | replServer.setupHistory('.repl_history', (err, repl) => {}); 25 | 26 | const context = { 27 | Package, 28 | PackageVersion, 29 | SubscriberPackageVersion, 30 | Package1Version, 31 | SfProject, 32 | getConnection: async (username) => { 33 | const org = await Org.create({ aliasOrUsername: username }); 34 | return org.getConnection(); 35 | }, 36 | }; 37 | 38 | Object.assign(replServer.context, context); 39 | -------------------------------------------------------------------------------- /.github/workflows/notify-slack-on-pr-open.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Slack Notification 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Notify Slack on PR open 12 | env: 13 | WEBHOOK_URL: ${{ secrets.CLI_TEAM_SLACK_WEBHOOK_URL }} 14 | PULL_REQUEST_AUTHOR_ICON_URL: ${{ github.event.pull_request.user.avatar_url }} 15 | PULL_REQUEST_AUTHOR_NAME: ${{ github.event.pull_request.user.login }} 16 | PULL_REQUEST_AUTHOR_PROFILE_URL: ${{ github.event.pull_request.user.html_url }} 17 | PULL_REQUEST_BASE_BRANCH_NAME: ${{ github.event.pull_request.base.ref }} 18 | PULL_REQUEST_COMPARE_BRANCH_NAME: ${{ github.event.pull_request.head.ref }} 19 | PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} 20 | PULL_REQUEST_REPO: ${{ github.event.pull_request.head.repo.name }} 21 | PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }} 22 | PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }} 23 | uses: salesforcecli/github-workflows/.github/actions/prNotification@main 24 | -------------------------------------------------------------------------------- /messages/bundle_version_create.md: -------------------------------------------------------------------------------- 1 | # failedToGetPackageBundleVersionCreateStatus 2 | 3 | Failed to get package bundle version create status 4 | 5 | # failedToCreatePackageBundleVersion 6 | 7 | Failed to create package bundle version 8 | 9 | # bundleVersionComponentsMustBeArray 10 | 11 | Bundle version components must be an array of objects with packageVersion property 12 | 13 | # bundleVersionComponentMustBeObject 14 | 15 | Each bundle version component must be an object with a packageVersion property 16 | 17 | # noPackageVersionFoundWithAlias 18 | 19 | No package version found with alias: %s 20 | 21 | # failedToReadBundleVersionComponentsFile 22 | 23 | Failed to read or parse bundle version components file 24 | 25 | # noPackageBundleFoundWithAlias 26 | 27 | No package bundle found with alias: %s 28 | 29 | # noBundleFoundWithId 30 | 31 | No bundle found with id: %s 32 | 33 | # noBundleFoundWithName 34 | 35 | No bundle found with name: %s 36 | 37 | # invalidVersionNumberFormat 38 | 39 | Invalid version number format: %s 40 | 41 | # majorVersionMismatch 42 | 43 | Major version mismatch: expected %s, found %s 44 | 45 | # invalidMinorVersionInExisting 46 | 47 | Invalid minor version in existing record: %s 48 | -------------------------------------------------------------------------------- /.github/workflows/create-github-release.yml: -------------------------------------------------------------------------------- 1 | name: create-github-release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - prerelease/** 8 | tags-ignore: 9 | - '*' 10 | workflow_dispatch: 11 | inputs: 12 | prerelease: 13 | type: string 14 | description: 'Name to use for the prerelease: beta, dev, etc. NOTE: If this is already set in the package.json, it does not need to be passed in here.' 15 | 16 | jobs: 17 | release: 18 | uses: salesforcecli/github-workflows/.github/workflows/create-github-release.yml@main 19 | secrets: inherit 20 | with: 21 | prerelease: ${{ inputs.prerelease }} 22 | # If this is a push event, we want to skip the release if there are no semantic commits 23 | # However, if this is a manual release (workflow_dispatch), then we want to disable skip-on-empty 24 | # This helps recover from forgetting to add semantic commits ('fix:', 'feat:', etc.) 25 | skip-on-empty: ${{ github.event_name == 'push' }} 26 | docs: 27 | # Most repos won't use this 28 | # Depends on the 'release' job to avoid git collisions, not for any functionality reason 29 | needs: release 30 | secrets: inherit 31 | if: ${{ github.ref_name == 'main' }} 32 | uses: salesforcecli/github-workflows/.github/workflows/publishTypedoc.yml@main 33 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/README.md: -------------------------------------------------------------------------------- 1 | # Salesforce DX Project: Next Steps 2 | 3 | Now that you’ve created a Salesforce DX project, what’s next? Here are some documentation resources to get you started. 4 | 5 | ## How Do You Plan to Deploy Your Changes? 6 | 7 | Do you want to deploy a set of changes, or create a self-contained application? Choose a [development model](https://developer.salesforce.com/tools/vscode/en/user-guide/development-models). 8 | 9 | ## Configure Your Salesforce DX Project 10 | 11 | The `sfdx-project.json` file contains useful configuration information for your project. See [Salesforce DX Project Configuration](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm) in the _Salesforce DX Developer Guide_ for details about this file. 12 | 13 | ## Read All About It 14 | 15 | - [Salesforce Extensions Documentation](https://developer.salesforce.com/tools/vscode/) 16 | - [Salesforce CLI Setup Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_setup.meta/sfdx_setup/sfdx_setup_intro.htm) 17 | - [Salesforce DX Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_intro.htm) 18 | - [Salesforce CLI Command Reference](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/cli_reference.htm) 19 | -------------------------------------------------------------------------------- /test/package/resources/packageProject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "salesforce-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Salesforce App", 6 | "scripts": { 7 | "lint": "npm run lint:lwc && npm run lint:aura", 8 | "lint:aura": "eslint **/aura/**", 9 | "lint:lwc": "eslint **/lwc/**", 10 | "test": "npm run test:unit", 11 | "test:unit": "sfdx-lwc-jest", 12 | "test:unit:watch": "sfdx-lwc-jest --watch", 13 | "test:unit:debug": "sfdx-lwc-jest --debug", 14 | "test:unit:coverage": "sfdx-lwc-jest --coverage", 15 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"", 16 | "prettier:verify": "prettier --list-different \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,xml,yaml,yml}\"" 17 | }, 18 | "devDependencies": { 19 | "@prettier/plugin-xml": "^0.12.0", 20 | "@salesforce/eslint-config-lwc": "^0.7.0", 21 | "@salesforce/eslint-plugin-aura": "^1.4.0", 22 | "@salesforce/sfdx-lwc-jest": "^0.9.2", 23 | "eslint": "^7.6.0", 24 | "eslint-config-prettier": "^6.11.0", 25 | "husky": "^4.2.1", 26 | "lint-staged": "^10.0.7", 27 | "prettier": "^2.0.5", 28 | "prettier-plugin-apex": "^1.6.0" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "lint-staged" 33 | } 34 | }, 35 | "lint-staged": { 36 | "**/*.{cls,cmp,component,css,html,json,md,page,trigger,xml,yaml,yml}": [ 37 | "prettier --write" 38 | ], 39 | "**/{aura|lwc}/**": [ 40 | "eslint" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Packaging 2 | 3 | [![NPM](https://img.shields.io/npm/v/@salesforce/packaging.svg?label=@salesforce/packaging)](https://www.npmjs.com/package/@salesforce/packaging) [![Downloads/week](https://img.shields.io/npm/dw/@salesforce/packaging.svg)](https://npmjs.org/package/@salesforce/packaging) [![License](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](https://opensource.org/license/apache-2-0) 4 | 5 | ## Description 6 | 7 | A TypeScript library for packaging metadata in your Salesforce project. This library supports both First-Generation packaging (1GP) and Second-Generation packaging (2GP). 8 | 9 | ## Usage 10 | 11 | There are 4 main classes to use from this library: 12 | 13 | 1. [Package1Version](https://forcedotcom.github.io/packaging/classes/package1_package1Version.Package1Version.html) - Work with 1st generation package versions. 14 | 1. [Package](https://forcedotcom.github.io/packaging/classes/package_package.Package.html) - Work with 2nd generation packages. 15 | 1. [PackageVersion](https://forcedotcom.github.io/packaging/classes/package_packageVersion.PackageVersion.html) - Work with 2nd generation package versions. 16 | 1. [SubscriberPackageVersion](https://forcedotcom.github.io/packaging/classes/package_subscriberPackageVersion.SubscriberPackageVersion.html) - Work with 2nd generation subscriber package versions. 17 | 18 | Please reference the [API Documentation](https://forcedotcom.github.io/packaging/) for complete details of code and types. 19 | 20 | ## Contributing 21 | 22 | If you are interested in contributing, please take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide. 23 | -------------------------------------------------------------------------------- /src/package/packageVersionCreateRequestReport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Connection } from '@salesforce/core'; 18 | import { PackageVersionCreateRequestResult } from '../interfaces'; 19 | import * as pkgUtils from '../utils/packageUtils'; 20 | import { applyErrorAction, massageErrorMessage } from '../utils/packageUtils'; 21 | import { byId } from './packageVersionCreateRequest'; 22 | 23 | export async function getCreatePackageVersionCreateRequestReport(options: { 24 | createPackageVersionRequestId: string; 25 | connection: Connection; 26 | }): Promise { 27 | try { 28 | pkgUtils.validateId(pkgUtils.BY_LABEL.PACKAGE_VERSION_CREATE_REQUEST_ID, options.createPackageVersionRequestId); 29 | const results = await byId(options.createPackageVersionRequestId, options.connection); 30 | return results[0]; 31 | } catch (err) { 32 | if (err instanceof Error) { 33 | throw applyErrorAction(massageErrorMessage(err)); 34 | } 35 | throw err; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | yarn-lockfile-check: 9 | uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main 10 | # Since the Windows unit tests take much longer, we run the linux unit tests first and then run the windows unit tests in parallel with NUTs 11 | linux-unit-tests: 12 | needs: yarn-lockfile-check 13 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main 14 | windows-unit-tests: 15 | needs: linux-unit-tests 16 | uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main 17 | nuts: 18 | needs: linux-unit-tests 19 | uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main 20 | secrets: inherit 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest] 24 | fail-fast: false 25 | with: 26 | os: ${{ matrix.os }} 27 | xNuts: 28 | needs: linux-unit-tests 29 | uses: salesforcecli/github-workflows/.github/workflows/externalNut.yml@main 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | os: ['ubuntu-latest', 'windows-latest'] 34 | with: 35 | packageName: '@salesforce/packaging' 36 | externalProjectGitUrl: 'https://github.com/salesforcecli/plugin-packaging' 37 | command: 'yarn test:nuts:package' 38 | os: ${{ matrix.os }} 39 | preSwapCommands: 'yarn upgrade @salesforce/core; yarn upgrade @jsforce/jsforce-node@latest; npx yarn-deduplicate; yarn install' 40 | preExternalBuildCommands: 'npm why @salesforce/core --json' 41 | useCache: false 42 | secrets: inherit 43 | -------------------------------------------------------------------------------- /messages/package_version.md: -------------------------------------------------------------------------------- 1 | # errorInvalidIdNoMatchingVersionId 2 | 3 | The %s %s is invalid, as a corresponding %s was not found 4 | 5 | # errorInvalidPackageVersionId 6 | 7 | The provided alias or ID: [%s] could not be resolved to a valid package version ID (05i) or subscriber package version ID (04t). 8 | 9 | # errorInvalidPackageVersionIdNoProject 10 | 11 | The provided alias or ID: [%s] could not be resolved to a valid package version ID (05i) or subscriber package version ID (04t). 12 | 13 | # errorInvalidPackageVersionIdNoProject.actions 14 | 15 | If you are using a package alias, make sure you are inside your sfdx project and it's defined in the `packageDirectories` section in `sfdx-project.json` 16 | 17 | # packageAliasNotFound 18 | 19 | The provided package ID: [%s] could not be resolved to an alias. 20 | 21 | # createResultIdCannotBeEmpty 22 | 23 | The package version create result ID must be defined when checking for completion. 24 | 25 | # errorNoSubscriberPackageVersionId 26 | 27 | Could not fetch the subscriber package version ID (04t). 28 | 29 | # maxPackage2VersionRecords 30 | 31 | The maximum result size (2000) was reached when querying the Package2Version SObject. This means there could be more records that were not returned by the query. If all records are required you may have to break the query into multiple requests filtered by date, then aggregate the results. 32 | 33 | # errors.RequiresProject 34 | 35 | This method expects an sfdx project to be available to write the new package version data in it. 36 | Make sure to pass `options.project` when instantiating `PackageVersion`. 37 | https://forcedotcom.github.io/packaging/classes/package_packageVersion.PackageVersion.html#constructor 38 | -------------------------------------------------------------------------------- /src/package/packageDelete.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Connection, SfProject } from '@salesforce/core'; 18 | import { applyErrorAction, BY_LABEL, combineSaveErrors, massageErrorMessage, validateId } from '../utils/packageUtils'; 19 | import { PackageSaveResult } from '../interfaces'; 20 | 21 | export async function deletePackage( 22 | idOrAlias: string, 23 | project: SfProject | undefined, 24 | connection: Connection, 25 | undelete: boolean 26 | ): Promise { 27 | const packageId = project ? project.getPackageIdFromAlias(idOrAlias) ?? idOrAlias : idOrAlias; 28 | validateId(BY_LABEL.PACKAGE_ID, packageId); 29 | 30 | const request = { Id: packageId, IsDeprecated: !undelete }; 31 | 32 | const updateResult = await connection.tooling.update('Package2', request).catch((err) => { 33 | if (err instanceof Error) { 34 | throw applyErrorAction(massageErrorMessage(err)); 35 | } 36 | throw err; 37 | }); 38 | if (!updateResult.success) { 39 | throw combineSaveErrors('Package2', 'update', updateResult.errors); 40 | } 41 | return updateResult; 42 | } 43 | -------------------------------------------------------------------------------- /src/interfaces/bundleInterfacesAndType.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Nullable } from '@salesforce/ts-types'; 18 | import { Connection, SfProject } from '@salesforce/core'; 19 | import { Duration } from '@salesforce/kit'; 20 | import { SaveResult } from '@jsforce/jsforce-node'; 21 | 22 | export type BundleCreateOptions = { 23 | BundleName: string; 24 | Description: string; 25 | }; 26 | 27 | export type BundleVersionCreateOptions = { 28 | connection: Connection; 29 | project: SfProject; 30 | PackageBundle: string; 31 | MajorVersion: string; 32 | MinorVersion: string; 33 | Ancestor: Nullable; 34 | BundleVersionComponentsPath: string; 35 | Description?: string; // Optional description for the bundle version 36 | polling?: { 37 | timeout: Duration; 38 | frequency: Duration; 39 | }; 40 | }; 41 | 42 | export type BundleInstallOptions = { 43 | connection: Connection; 44 | project: SfProject; 45 | PackageBundleVersion: string; 46 | DevelopmentOrganization: string; 47 | polling?: { 48 | timeout: Duration; 49 | frequency: Duration; 50 | }; 51 | }; 52 | 53 | export type BundleSaveResult = SaveResult; 54 | -------------------------------------------------------------------------------- /.github/workflows/failureNotifications.yml: -------------------------------------------------------------------------------- 1 | name: failureNotifications 2 | on: 3 | workflow_run: 4 | workflows: 5 | - publish 6 | types: 7 | - completed 8 | jobs: 9 | failure-notify: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 12 | steps: 13 | - name: Announce Failure 14 | id: slack 15 | uses: slackapi/slack-github-action@v1.26.0 16 | env: 17 | # for non-CLI-team-owned plugins, you can send this anywhere you like 18 | SLACK_WEBHOOK_URL: ${{ secrets.CLI_ALERTS_SLACK_WEBHOOK }} 19 | SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK 20 | with: 21 | # Payload can be visually tested here: https://app.slack.com/block-kit-builder/T01GST6QY0G#%7B%22blocks%22:%5B%5D%7D 22 | # Only copy over the "blocks" array to the Block Kit Builder 23 | payload: | 24 | { 25 | "text": "Workflow \"${{ github.event.workflow_run.name }}\" failed in ${{ github.event.workflow_run.repository.name }}", 26 | "blocks": [ 27 | { 28 | "type": "header", 29 | "text": { 30 | "type": "plain_text", 31 | "text": ":bh-alert: Workflow \"${{ github.event.workflow_run.name }}\" failed in ${{ github.event.workflow_run.repository.name }} :bh-alert:" 32 | } 33 | }, 34 | { 35 | "type": "section", 36 | "text": { 37 | "type": "mrkdwn", 38 | "text": "*Repo:* ${{ github.event.workflow_run.repository.html_url }}\n*Workflow name:* `${{ github.event.workflow_run.name }}`\n*Job url:* ${{ github.event.workflow_run.html_url }}" 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /messages/pkg_utils.md: -------------------------------------------------------------------------------- 1 | # errorDuringSObjectCRUDOperation 2 | 3 | An error occurred during CRUD operation %s on entity %s. 4 | %s 5 | 6 | # errorInvalidIdNoMatchingVersionId 7 | 8 | The %s %s is invalid, as a corresponding %s was not found 9 | 10 | # invalidPackageTypeAction 11 | 12 | Specify Unlocked or Managed for package type. 13 | 14 | # invalidPackageTypeMessage 15 | 16 | Invalid package type 17 | 18 | # idNotFoundAction 19 | 20 | It`s possible that this package was created on a different Dev Hub. Authenticate to the Dev Hub org that owns the package, and reference that Dev Hub when running the command. 21 | 22 | # malformedPackageVersionIdAction 23 | 24 | Use "sfdx force:package:version:list" to verify the 05i package version ID. 25 | 26 | # malformedPackageVersionIdMessage 27 | 28 | We can’t find this package version ID for this Dev Hub. 29 | 30 | # malformedPackageIdAction 31 | 32 | Use "sfdx force:package:list" to verify the 0Ho package version ID. 33 | 34 | # malformedPackageIdMessage 35 | 36 | We can’t find the package id in the Dev Hub. 37 | 38 | # notFoundMessage 39 | 40 | The requested resource does not exist 41 | 42 | # itemDoesNotFitWithinMaxLength 43 | 44 | When calculating the number of items to be included in query "%s", when formatted, was too long. 45 | The item was (truncated): %s with a length of %s. The maximum length of items, when formatted is %s. 46 | 47 | # packageNotEnabledAction 48 | 49 | Packaging is not enabled on this org. Verify that you are authenticated to the desired org and try again. Otherwise, contact Salesforce Customer Support for more information. 50 | 51 | # packageInstanceNotEnabled 52 | 53 | Your org does not have permission to specify a build instance for your package version. Verify that you are authenticated to the desired org and try again. Otherwise, contact Salesforce Customer Support for more information. 54 | 55 | # packageSourceOrgNotEnabled 56 | 57 | Your Dev Hub does not have permission to specify a source org for your build org. Verify that you are authenticated to the correct Dev Hub and try again. Otherwise, contact Salesforce Customer Support for assistance. 58 | 59 | # invalidIdOrAlias 60 | 61 | The %s: %s isn't defined in the sfdx-project.json. Add it to the packageDirectories section and add the alias to packageAliases with its %s ID. 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. The [DEVELOPING](DEVELOPING.md) doc has details on how to set up your environment. 4 | 1. Familiarize yourself with the codebase by reading the [docs](https://forcedotcom.github.io/packaging/), which you can generate locally by running `yarn docs`. 5 | 1. Create a new issue before starting your project so that we can keep track of 6 | what you are trying to add/fix. That way, we can also offer suggestions or 7 | let you know if there is already an effort in progress. 8 | 1. Fork this repository (external contributors) or branch off main (committers). 9 | 1. Create a _topic_ branch in your fork based on the main branch. Note, this step is recommended but technically not required if contributing using a fork. 10 | 1. Edit the code in your fork/branch. 11 | 1. Write appropriate tests for your changes. Try to achieve at least 95% code coverage on any new code. No pull request will be accepted without associated tests. 12 | 1. Sign CLA (see [CLA](#cla) below). 13 | 1. Send us a pull request when you are done. We'll review your code, suggest any 14 | needed changes, and merge it in. 15 | 1. Upon merge, a new release of the `@salesforce/packaging` library will be published to npmjs with a version bump corresponding to commitizen rules. 16 | 17 | ### CLA 18 | 19 | External contributors will be required to sign a Contributor's License 20 | Agreement. You can do so by going to https://cla.salesforce.com/sign-cla. 21 | 22 | ## Branches 23 | 24 | - We work in branches off of `main`. 25 | - Our released (aka. _production_) branch is `main`. 26 | - Our work happens in _topic_ branches (feature and/or bug-fix). 27 | - feature as well as bug-fix branches are based on `main` 28 | - branches _should_ be kept up-to-date using `rebase` 29 | - [commit messages are enforced](DEVELOPING.md#When-you-are-ready-to-commit) 30 | 31 | ## Pull Requests 32 | 33 | - Develop features and bug fixes in _topic_ branches off main, or forks. 34 | - _Topic_ branches can live in forks (external contributors) or within this repository (committers). 35 | \*\* When creating _topic_ branches in this repository please prefix with `/`. 36 | - PRs will be reviewed and merged by committers. 37 | 38 | ## Releasing 39 | 40 | - A new version of this library (`@salesforce/packaging`) will be published upon merging PRs to `main`, with the version number increment based on commitizen rules. E.g., if any commit message begins with, "feat:" the minor version will be bumped. If any commit message begins with, "fix:" the patch version will be bumped. 41 | -------------------------------------------------------------------------------- /src/package/packageBundleCreate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Connection, Messages, SfError, SfProject } from '@salesforce/core'; 17 | import { BundleEntry } from '@salesforce/schemas'; 18 | import { BundleSObjects, BundleCreateOptions } from '../interfaces'; 19 | import { massageErrorMessage } from '../utils/bundleUtils'; 20 | 21 | Messages.importMessagesDirectory(__dirname); 22 | const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_create'); 23 | 24 | type Bundle2Request = Pick; 25 | 26 | export function createPackageDirEntry(project: SfProject, options: BundleCreateOptions): BundleEntry { 27 | return { 28 | versionName: 'ver 0.1', 29 | versionNumber: '0.1', 30 | name: options.BundleName, 31 | versionDescription: options.Description, 32 | }; 33 | } 34 | 35 | export async function createBundle( 36 | connection: Connection, 37 | project: SfProject, 38 | options: BundleCreateOptions 39 | ): Promise<{ Id: string }> { 40 | const request: Bundle2Request = { BundleName: options.BundleName, Description: options.Description }; 41 | let createResult; 42 | try { 43 | createResult = await connection.tooling.sobject('PackageBundle').create(request); 44 | } catch (err) { 45 | const error = 46 | err instanceof Error 47 | ? err 48 | : new Error(typeof err === 'string' ? err : messages.getMessage('failedToCreatePackageBundle')); 49 | throw SfError.wrap(massageErrorMessage(error)); 50 | } 51 | 52 | if (!createResult?.success) { 53 | throw SfError.wrap(massageErrorMessage(new Error(messages.getMessage('failedToCreatePackageBundle')))); 54 | } 55 | 56 | const bundleEntry = createPackageDirEntry(project, options); 57 | project.getSfProjectJson().addPackageBundle(bundleEntry); 58 | project.getSfProjectJson().addPackageBundleAlias(bundleEntry.name, createResult.id); 59 | await project.getSfProjectJson().write(); 60 | 61 | return { Id: createResult.id }; 62 | } 63 | -------------------------------------------------------------------------------- /test/package1/packageDisplay.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Connection } from '@salesforce/core'; 17 | import { instantiateContext, MockTestOrgData, restoreContext, stubContext } from '@salesforce/core/testSetup'; 18 | import { expect } from 'chai'; 19 | import { Package1Version } from '../../src/package1'; 20 | 21 | describe('Package1 Display', () => { 22 | const testOrg = new MockTestOrgData(); 23 | const $$ = instantiateContext(); 24 | let conn: Connection; 25 | let queryStub: sinon.SinonStub; 26 | 27 | beforeEach(async () => { 28 | stubContext($$); 29 | await $$.stubAuths(testOrg); 30 | conn = await testOrg.getConnection(); 31 | queryStub = $$.SANDBOX.stub(conn.tooling, 'query'); 32 | }); 33 | 34 | afterEach(() => { 35 | restoreContext($$); 36 | }); 37 | 38 | it('should query and collate data correctly', async () => { 39 | queryStub.resolves({ 40 | done: true, 41 | totalSize: 1, 42 | records: [ 43 | { 44 | Id: '04t46000001ZfaXXXX', 45 | Name: 'Summer 22', 46 | MetadataPackageId: '03346000000dmo4XXX', 47 | MajorVersion: 1, 48 | MinorVersion: 0, 49 | PatchVersion: 3, 50 | ReleaseState: 'Beta', 51 | BuildNumber: 1, 52 | }, 53 | ], 54 | }); 55 | const pv1 = new Package1Version(conn, '04t46000001ZfaXXXX'); 56 | const result = await pv1.getPackageVersion(); 57 | expect(result).deep.equal([ 58 | { 59 | BuildNumber: 1, 60 | Id: '04t46000001ZfaXXXX', 61 | MajorVersion: 1, 62 | MetadataPackageId: '03346000000dmo4XXX', 63 | MinorVersion: 0, 64 | Name: 'Summer 22', 65 | PatchVersion: 3, 66 | ReleaseState: 'Beta', 67 | }, 68 | ]); 69 | }); 70 | 71 | it('should query and collate data correctly - no results', async () => { 72 | queryStub.resolves({ 73 | done: true, 74 | totalSize: 0, 75 | records: [], 76 | }); 77 | const pv1 = new Package1Version(conn, '04t46000001ZfaXXXX'); 78 | const result = await pv1.getPackageVersion(); 79 | expect(result).deep.equal([]); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /messages/package.md: -------------------------------------------------------------------------------- 1 | # invalidPackageId 2 | 3 | The id [%s] is invalid. It must start with "%s". 4 | 5 | # defaultErrorMessage 6 | 7 | Can't uninstall the package %s during uninstall request %s. 8 | 9 | # action 10 | 11 | Verify installed package ID and resolve errors, then try again. 12 | 13 | # couldNotFindAliasForId 14 | 15 | Could not find an alias for the ID %s. 16 | 17 | # packageAliasNotFound 18 | 19 | Package alias %s not found in project. 20 | 21 | # packageNotFound 22 | 23 | A package with id %s was not found. 24 | 25 | # appAnalyticsEnabledApiPriorTo59Error 26 | 27 | Enabling App Analytics is only possible with API version 59.0 or higher. 28 | 29 | # sourcesDownloadDirectoryNotEmpty 30 | 31 | Can't retrieve package version metadata. The specified directory isn't empty. Empty the directory, or create a new one and try again. 32 | 33 | # sourcesDownloadDirectoryMustBeRelative 34 | 35 | Can't retrieve package version metadata. The specified directory must be relative to your Salesforce DX project directory, and not an absolute path. 36 | 37 | # developerUsePkgZipFieldUnavailable 38 | 39 | Can't retrieve package metadata. To use this feature, you must first assign yourself the DownloadPackageVersionZips user permission. Then retry retrieving your package metadata. 40 | 41 | # downloadDeveloperPackageZipHasNoData 42 | 43 | Can't retrieve package metadata. We're unable to retrieve metadata for the package version you specified. Retrieving package metadata is available to converted 2GP package versions only. If the package you specified is a converted 2GP package, try creating a new package version, and then retry retrieving the package metadata for the new package version. If your package is a 1GP, start by converting the package to 2GP, and then retry retrieving metadata from the converted 2GP package version. 44 | 45 | # packagingNotEnabledOnOrg 46 | 47 | Can't retrieve package metadata. The org you specified doesn't have the required second-generation packaging permission enabled. Enable this permission on your Dev Hub org, and try again. 48 | 49 | # recommendedVersionIdApiPriorTo66Error 50 | 51 | To enable Recommended Version, use API version 66.0 or higher. 52 | 53 | # skipAncestorCheckRequiresRecommendedVersionIdError 54 | 55 | The skip ancestor check requires a recommended version ID. 56 | 57 | # noPackageVersionsForGivenPackage2FoundError 58 | 59 | No package versions were found for the given Package 2 ID (0Ho). At least one released package version must exist. 60 | 61 | # recommendedVersionNotAncestorOfPriorVersionError 62 | 63 | The new recommended version is not a descendant of the previous recommended version. To bypass this check, use the --skip-ancestor-check CLI flag. 64 | 65 | # invalidRecommendedVersionError 66 | 67 | Provide a valid subscriber package version (04t) for the recommended version. 68 | 69 | # unassociatedRecommendedVersionError 70 | 71 | The provided recommended version isn't associated with this package. 72 | -------------------------------------------------------------------------------- /test/package/bundleList.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import { expect } from 'chai'; 19 | import { Connection, SfProject, SfError } from '@salesforce/core'; 20 | import { instantiateContext, restoreContext, stubContext, MockTestOrgData } from '@salesforce/core/testSetup'; 21 | import { AnyJson, ensureJsonMap } from '@salesforce/ts-types'; 22 | import { ensureString } from '@salesforce/ts-types'; 23 | import { PackageBundle } from '../../src/package/packageBundle'; 24 | 25 | async function setupProject(setup: (project: SfProject) => void = () => {}) { 26 | const project = await SfProject.resolve(); 27 | 28 | setup(project); 29 | const projectDir = project.getPath(); 30 | project 31 | .getSfProjectJson() 32 | .getContents() 33 | .packageDirectories?.forEach((dir) => { 34 | if (dir.path) { 35 | const packagePath = path.join(projectDir, dir.path); 36 | fs.mkdirSync(packagePath, { recursive: true }); 37 | } 38 | }); 39 | 40 | return project; 41 | } 42 | 43 | describe('bundleList', () => { 44 | const testContext = instantiateContext(); 45 | const testOrg = new MockTestOrgData(); 46 | let connection: Connection; 47 | 48 | beforeEach(async () => { 49 | stubContext(testContext); 50 | connection = await testOrg.getConnection(); 51 | }); 52 | 53 | afterEach(() => { 54 | restoreContext(testContext); 55 | }); 56 | 57 | describe('list bundles', () => { 58 | it('should list package bundles successfully', async () => { 59 | testContext.inProject(true); 60 | await setupProject((proj) => { 61 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 62 | }); 63 | 64 | const mockBundle = { 65 | BundleName: 'testBundle', 66 | Description: 'testBundle', 67 | Id: '0Ho000000000000', 68 | IsDeleted: false, 69 | CreatedDate: '2024-01-01T00:00:00.000+0000', 70 | CreatedById: '005000000000000', 71 | LastModifiedDate: '2024-01-01T00:00:00.000+0000', 72 | LastModifiedById: '005000000000000', 73 | SystemModstamp: '2024-01-01T00:00:00.000+0000', 74 | }; 75 | 76 | testContext.fakeConnectionRequest = (request: AnyJson): Promise => { 77 | const requestMap = ensureJsonMap(request); 78 | if (request && ensureString(requestMap.url).includes('PackageBundle')) { 79 | return Promise.resolve({ 80 | done: true, 81 | totalSize: 1, 82 | records: [mockBundle], 83 | }); 84 | } else { 85 | return Promise.reject(new SfError(`Unexpected request: ${String(requestMap.url)}`)); 86 | } 87 | }; 88 | 89 | const bundles = await PackageBundle.list(connection); 90 | expect(bundles).to.deep.equal([mockBundle]); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/package/versionNumber.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { expect } from 'chai'; 17 | import { VersionNumber } from '../../src/package/versionNumber'; 18 | 19 | describe('VersionNumber', () => { 20 | it('should be able to parse a version number', () => { 21 | const version = VersionNumber.from('1.2.3.4'); 22 | expect(version.major).to.be.equal('1'); 23 | expect(version.minor).to.be.equal('2'); 24 | expect(version.patch).to.be.equal('3'); 25 | expect(version.build).to.be.equal('4'); 26 | expect(version.toString()).to.be.equal('1.2.3.4'); 27 | }); 28 | it('should be able to parse a version number with build token', () => { 29 | const version = VersionNumber.from('1.2.3.NONE'); 30 | expect(version.major).to.be.equal('1'); 31 | expect(version.minor).to.be.equal('2'); 32 | expect(version.patch).to.be.equal('3'); 33 | expect(version.build).to.be.equal('NONE'); 34 | expect(version.toString()).to.be.equal('1.2.3.NONE'); 35 | }); 36 | it('should throw if version number does not have four nodes', () => { 37 | expect(() => VersionNumber.from('1.2.3')).to.throw( 38 | Error, 39 | 'VersionNumber must be in the format major.minor.patch.build but the value found is [1.2.3]' 40 | ); 41 | }); 42 | it('should throw if version number does not contain numbers', () => { 43 | expect(() => VersionNumber.from('.a.b.')).to.throw( 44 | Error, 45 | 'VersionNumber parts major, minor or patch must be a number but the value found is [.a.b.].' 46 | ); 47 | }); 48 | it('should throw if version number build token is invalid', () => { 49 | expect(() => VersionNumber.from('1.2.3.none')).to.throw( 50 | Error, 51 | "The provided VersionNumber '1.2.3.none' is invalid. Build number token must be a number or one of these tokens 'LATEST, NEXT, RELEASED, HIGHEST, NONE'." 52 | ); 53 | }); 54 | it('should throw if version number undefined', () => { 55 | expect(() => VersionNumber.from(undefined)).to.throw(Error, 'The VersionNumber property must be specified.'); 56 | }); 57 | it('should sort version numbers', () => { 58 | const versions = [ 59 | VersionNumber.from('1.0.0.0'), 60 | VersionNumber.from('1.1.0.0'), 61 | VersionNumber.from('2.0.0.0'), 62 | VersionNumber.from('2.0.2.0'), 63 | VersionNumber.from('3.0.0.0'), 64 | VersionNumber.from('3.0.0.3'), 65 | VersionNumber.from('3.0.0.NONE'), 66 | ]; 67 | const sorted = [...versions].reverse().sort((a, b) => a.compareTo(b)); 68 | expect(sorted).to.deep.equal(versions); 69 | }); 70 | it('should be able to detect a well-known build number', () => { 71 | const version = VersionNumber.from('1.2.3.NONE'); 72 | expect(version.isBuildKeyword()).to.be.true; 73 | }); 74 | it('should be able to detect a numeric build number', () => { 75 | const version = VersionNumber.from('1.2.3.4'); 76 | expect(version.isBuildKeyword()).to.be.false; 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/package/packageBundle.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Connection, SfError, SfProject } from '@salesforce/core'; 18 | import { SaveResult, Schema } from '@jsforce/jsforce-node'; 19 | import { Duration } from '@salesforce/kit'; 20 | import { BundleCreateOptions, BundleSObjects, BundleVersionCreateOptions } from '../interfaces'; 21 | import { createBundle } from './packageBundleCreate'; 22 | import { PackageBundleVersion } from './packageBundleVersion'; 23 | 24 | const BundleFields = [ 25 | 'BundleName', 26 | 'Description', 27 | 'Id', 28 | 'IsDeleted', 29 | 'CreatedDate', 30 | 'CreatedById', 31 | 'LastModifiedDate', 32 | 'LastModifiedById', 33 | 'SystemModstamp', 34 | ]; 35 | 36 | export class PackageBundle { 37 | /** 38 | * Create a new package bundle. 39 | * 40 | * @param connection - instance of Connection 41 | * @param project - instance of SfProject 42 | * @param options - options for creating a bundle - see BundleCreateOptions 43 | * @returns PackageBundle 44 | */ 45 | public static async create( 46 | connection: Connection, 47 | project: SfProject, 48 | options: BundleCreateOptions 49 | ): Promise<{ Id: string }> { 50 | return createBundle(connection, project, options); 51 | } 52 | 53 | /** 54 | * Create a new package bundle version. 55 | * 56 | * @param connection - instance of Connection 57 | * @param project - instance of SfProject 58 | * @param options - options for creating a bundle version - see BundleVersionCreateOptions 59 | * @returns PackageBundle 60 | */ 61 | public static async createVersion( 62 | options: BundleVersionCreateOptions, 63 | polling: { frequency: Duration; timeout: Duration } = { 64 | frequency: Duration.seconds(0), 65 | timeout: Duration.seconds(0), 66 | } 67 | ): Promise { 68 | return PackageBundleVersion.create({ ...options, polling }); 69 | } 70 | 71 | public static async delete(connection: Connection, project: SfProject, idOrAlias: string): Promise { 72 | // Check if it's already an ID (1Fl followed by 15 characters) 73 | if (/^1Fl.{15}$/.test(idOrAlias)) { 74 | return connection.tooling.sobject('PackageBundle').delete(idOrAlias); 75 | } 76 | 77 | // Validate that project is provided when using aliases 78 | if (!project) { 79 | throw new SfError('Project instance is required when deleting package bundle by alias'); 80 | } 81 | // eslint-disable-next-line no-param-reassign 82 | idOrAlias = project.getPackageBundleIdFromAlias(idOrAlias) ?? idOrAlias; 83 | 84 | return connection.tooling.sobject('PackageBundle').delete(idOrAlias); 85 | } 86 | 87 | /** 88 | * Returns all the package bundles that are available in the org, up to 10,000. If more records are 89 | * needed use the `SF_ORG_MAX_QUERY_LIMIT` env var. 90 | * 91 | * @param connection 92 | */ 93 | public static async list(connection: Connection): Promise { 94 | const query = `select ${BundleFields.join(', ')} from PackageBundle ORDER BY BundleName`; 95 | return (await connection.autoFetchQuery(query, { tooling: true }))?.records; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/package/bundleVersionList.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import { expect } from 'chai'; 19 | import { Connection, SfProject, SfError } from '@salesforce/core'; 20 | import { instantiateContext, restoreContext, stubContext, MockTestOrgData } from '@salesforce/core/testSetup'; 21 | import { AnyJson, ensureJsonMap } from '@salesforce/ts-types'; 22 | import { ensureString } from '@salesforce/ts-types'; 23 | import { PackageBundleVersion } from '../../src/package/packageBundleVersion'; 24 | 25 | async function setupProject(setup: (project: SfProject) => void = () => {}) { 26 | const project = await SfProject.resolve(); 27 | 28 | setup(project); 29 | const projectDir = project.getPath(); 30 | project 31 | .getSfProjectJson() 32 | .getContents() 33 | .packageDirectories?.forEach((dir) => { 34 | if (dir.path) { 35 | const packagePath = path.join(projectDir, dir.path); 36 | fs.mkdirSync(packagePath, { recursive: true }); 37 | } 38 | }); 39 | 40 | return project; 41 | } 42 | 43 | describe('bundleVersionList', () => { 44 | const testContext = instantiateContext(); 45 | const testOrg = new MockTestOrgData(); 46 | let connection: Connection; 47 | 48 | beforeEach(async () => { 49 | stubContext(testContext); 50 | connection = await testOrg.getConnection(); 51 | }); 52 | 53 | afterEach(() => { 54 | restoreContext(testContext); 55 | }); 56 | 57 | describe('list bundles', () => { 58 | it('should list package bundles successfully', async () => { 59 | testContext.inProject(true); 60 | await setupProject((proj) => { 61 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 62 | }); 63 | 64 | const mockBundleVersion = { 65 | Id: '0Ho000000000000', 66 | PackageBundle: { 67 | Id: '0Ho000000000001', 68 | BundleName: 'testBundle', 69 | Description: 'testBundle', 70 | IsDeleted: false, 71 | CreatedDate: '2024-01-01T00:00:00.000+0000', 72 | CreatedById: '005000000000000', 73 | LastModifiedDate: '2024-01-01T00:00:00.000+0000', 74 | LastModifiedById: '005000000000000', 75 | SystemModstamp: '2024-01-01T00:00:00.000+0000', 76 | }, 77 | VersionName: 'testBundle@1.0', 78 | MajorVersion: '1', 79 | MinorVersion: '0', 80 | CreatedDate: '2024-01-01T00:00:00.000+0000', 81 | CreatedById: '005000000000000', 82 | LastModifiedDate: '2024-01-01T00:00:00.000+0000', 83 | LastModifiedById: '005000000000000', 84 | IsReleased: false, 85 | Ancestor: null, 86 | }; 87 | 88 | testContext.fakeConnectionRequest = (request: AnyJson): Promise => { 89 | const requestMap = ensureJsonMap(request); 90 | if (request && ensureString(requestMap.url).includes('PackageBundle')) { 91 | return Promise.resolve({ 92 | done: true, 93 | totalSize: 1, 94 | records: [mockBundleVersion], 95 | }); 96 | } else { 97 | return Promise.reject(new SfError(`Unexpected request: ${String(requestMap.url)}`)); 98 | } 99 | }; 100 | 101 | const bundles = await PackageBundleVersion.list(connection); 102 | expect(bundles).to.deep.equal([mockBundleVersion]); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/package1/package1VersionList.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Connection } from '@salesforce/core'; 17 | import { instantiateContext, MockTestOrgData, restoreContext, stubContext } from '@salesforce/core/testSetup'; 18 | import { assert, expect } from 'chai'; 19 | import { Package1Version } from '../../src/package1'; 20 | 21 | const records = [ 22 | { 23 | Id: '04t46000001ZfaXXXX', 24 | Name: 'Summer 22', 25 | MetadataPackageId: '03346000000dmo4XXX', 26 | MajorVersion: 1, 27 | MinorVersion: 0, 28 | PatchVersion: 3, 29 | ReleaseState: 'Beta', 30 | BuildNumber: 1, 31 | }, 32 | { 33 | Id: '04t46000001ZfaXXXY', 34 | Name: 'Summer 22', 35 | MetadataPackageId: '03346000000dmo4XXX', 36 | MajorVersion: 1, 37 | MinorVersion: 0, 38 | PatchVersion: 4, 39 | ReleaseState: 'Beta', 40 | BuildNumber: 1, 41 | }, 42 | ]; 43 | 44 | const listResult = [ 45 | { 46 | BuildNumber: 1, 47 | Id: '04t46000001ZfaXXXX', 48 | MajorVersion: 1, 49 | MetadataPackageId: '03346000000dmo4XXX', 50 | MinorVersion: 0, 51 | Name: 'Summer 22', 52 | PatchVersion: 3, 53 | ReleaseState: 'Beta', 54 | }, 55 | { 56 | BuildNumber: 1, 57 | Id: '04t46000001ZfaXXXY', 58 | MajorVersion: 1, 59 | MetadataPackageId: '03346000000dmo4XXX', 60 | MinorVersion: 0, 61 | Name: 'Summer 22', 62 | PatchVersion: 4, 63 | ReleaseState: 'Beta', 64 | }, 65 | ]; 66 | 67 | describe('Package1 Version List', () => { 68 | const testOrg = new MockTestOrgData(); 69 | let conn: Connection; 70 | let queryStub: sinon.SinonStub; 71 | const $$ = instantiateContext(); 72 | 73 | beforeEach(async () => { 74 | stubContext($$); 75 | await $$.stubAuths(testOrg); 76 | conn = await testOrg.getConnection(); 77 | queryStub = $$.SANDBOX.stub(conn.tooling, 'query'); 78 | }); 79 | 80 | afterEach(() => { 81 | restoreContext($$); 82 | }); 83 | 84 | it('should query and collate data correctly', async () => { 85 | queryStub.resolves({ 86 | done: true, 87 | totalSize: 1, 88 | records, 89 | }); 90 | const result = await Package1Version.list(conn); 91 | expect(result).deep.equal(listResult); 92 | restoreContext($$); 93 | }); 94 | 95 | it('should query and collate data correctly with MetadataPackageId supplied', async () => { 96 | queryStub.resolves({ 97 | done: true, 98 | totalSize: 1, 99 | records: [records[0]], 100 | }); 101 | const result = await Package1Version.list(conn, '03346000000dmo4XXX'); 102 | expect(result).deep.equal([listResult[0]]); 103 | }); 104 | 105 | it('should query and collate data correctly - no results', async () => { 106 | queryStub.resolves({ 107 | done: true, 108 | totalSize: 0, 109 | records: [], 110 | }); 111 | const result = await Package1Version.list(conn, '03346000000dmo4XXX'); 112 | expect(result).deep.equal([]); 113 | }); 114 | 115 | it('should throw an error when invalid ID is provided', async () => { 116 | queryStub.resolves({ 117 | done: true, 118 | totalSize: 0, 119 | records: [], 120 | }); 121 | try { 122 | await Package1Version.list(conn, '04t46000001ZfaXXXX'); 123 | assert.fail('the above should throw an invalid id error'); 124 | } catch (e) { 125 | assert(e instanceof Error); 126 | expect(e.message).to.equal( 127 | 'Specify a valid package metadata package ID (starts with 033), received 04t46000001ZfaXXXX' 128 | ); 129 | } 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/package/packageCreate.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Connection, SfError, SfProject } from '@salesforce/core'; 18 | import { env } from '@salesforce/kit'; 19 | import { PackagePackageDir, PackageDir } from '@salesforce/schemas'; 20 | import { isPackagingDirectory } from '@salesforce/core/project'; 21 | import * as pkgUtils from '../utils/packageUtils'; 22 | import { applyErrorAction, massageErrorMessage } from '../utils/packageUtils'; 23 | import { PackageCreateOptions, PackagingSObjects } from '../interfaces'; 24 | 25 | type Package2Request = Pick< 26 | PackagingSObjects.Package2, 27 | 'Name' | 'Description' | 'NamespacePrefix' | 'ContainerOptions' | 'IsOrgDependent' | 'PackageErrorUsername' 28 | >; 29 | 30 | export function createPackageRequestFromContext(project: SfProject, options: PackageCreateOptions): Package2Request { 31 | const namespace = options.noNamespace ? '' : project.getSfProjectJson().getContents().namespace ?? ''; 32 | return { 33 | Name: options.name, 34 | Description: options.description, 35 | NamespacePrefix: namespace, 36 | ContainerOptions: options.packageType, 37 | IsOrgDependent: options.orgDependent, 38 | PackageErrorUsername: options.errorNotificationUsername, 39 | }; 40 | } 41 | 42 | /** 43 | * Create packageDirectory json entry for this package that can be written to sfdx-project.json 44 | * 45 | * @param project 46 | * @param options - package create options 47 | * @private 48 | */ 49 | 50 | export function createPackageDirEntry(project: SfProject, options: PackageCreateOptions): PackagePackageDir { 51 | const packageDirs: PackageDir[] = project.getSfProjectJson().getContents().packageDirectories ?? []; 52 | return { 53 | versionName: 'ver 0.1', 54 | versionNumber: '0.1.0.NEXT', 55 | ...(packageDirs 56 | .filter((pd: PackageDir) => pd.path === options.path && !isPackagingDirectory(pd)) 57 | .find((pd) => !('id' in pd)) ?? { 58 | // no match - create a new one 59 | path: options.path, 60 | default: packageDirs.length === 0 ? true : !packageDirs.some((pd) => pd.default === true), 61 | }), 62 | package: options.name, 63 | versionDescription: options.description, 64 | }; 65 | } 66 | 67 | export async function createPackage( 68 | connection: Connection, 69 | project: SfProject, 70 | options: PackageCreateOptions 71 | ): Promise<{ Id: string }> { 72 | const cleanOptions = sanitizePackageCreateOptions(options); 73 | const request = createPackageRequestFromContext(project, cleanOptions); 74 | const createResult = await connection.tooling 75 | .sobject('Package2') 76 | .create(request) 77 | .catch((err) => { 78 | const error = err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error'); 79 | throw SfError.wrap(applyErrorAction(massageErrorMessage(error))); 80 | }); 81 | 82 | if (!createResult.success) { 83 | throw pkgUtils.combineSaveErrors('Package2', 'create', createResult.errors); 84 | } 85 | 86 | if (!env.getBoolean('SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_CREATE')) { 87 | const packageDirectory = createPackageDirEntry(project, cleanOptions); 88 | project.getSfProjectJson().addPackageDirectory(packageDirectory); 89 | project.getSfProjectJson().addPackageAlias(cleanOptions.name, createResult.id); 90 | await project.getSfProjectJson().write(); 91 | } 92 | 93 | return { Id: createResult.id }; 94 | } 95 | 96 | /** strip trailing slash from path param */ 97 | const sanitizePackageCreateOptions = (options: PackageCreateOptions): PackageCreateOptions => ({ 98 | ...options, 99 | path: options.path.replace(/\/$/, ''), 100 | }); 101 | -------------------------------------------------------------------------------- /src/package/packageUninstall.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import os from 'node:os'; 17 | import { Connection, Lifecycle, Messages, PollingClient, SfError } from '@salesforce/core'; 18 | import { Duration } from '@salesforce/kit'; 19 | import { PackageEvents, PackagingSObjects } from '../interfaces'; 20 | import { applyErrorAction, combineSaveErrors, massageErrorMessage } from '../utils/packageUtils'; 21 | 22 | Messages.importMessagesDirectory(__dirname); 23 | const messages = Messages.loadMessages('@salesforce/packaging', 'package_uninstall'); 24 | const pkgMessages = Messages.loadMessages('@salesforce/packaging', 'package'); 25 | 26 | type UninstallResult = PackagingSObjects.SubscriberPackageVersionUninstallRequest; 27 | 28 | export async function getUninstallErrors(conn: Connection, id: string): Promise> { 29 | const errorQueryResult = await conn.tooling.query<{ Message: string }>( 30 | `"SELECT Message FROM PackageVersionUninstallRequestError WHERE ParentRequest.Id = '${id}' ORDER BY Message"` 31 | ); 32 | return errorQueryResult?.records ?? []; 33 | } 34 | 35 | export async function pollUninstall( 36 | uninstallRequestId: string, 37 | conn: Connection, 38 | frequency: Duration, 39 | wait: Duration 40 | ): Promise { 41 | const poll = async (id: string): Promise<{ completed: boolean; payload: UninstallResult }> => { 42 | const uninstallRequest = (await conn.tooling 43 | .sobject('SubscriberPackageVersionUninstallRequest') 44 | .retrieve(id)) as UninstallResult; 45 | 46 | switch (uninstallRequest.Status) { 47 | case 'Success': { 48 | return { completed: true, payload: uninstallRequest }; 49 | } 50 | case 'InProgress': 51 | case 'Queued': { 52 | await Lifecycle.getInstance().emit(PackageEvents.uninstall, { 53 | ...uninstallRequest, 54 | }); 55 | return { completed: false, payload: uninstallRequest }; 56 | } 57 | default: { 58 | const err = pkgMessages.getMessage('defaultErrorMessage', [id, uninstallRequest.Id]); 59 | const errorMessages = await getUninstallErrors(conn, id); 60 | 61 | const errors = errorMessages.map((error, index) => `(${index + 1}) ${error.Message}${os.EOL}`); 62 | const combinedErrors = errors.length ? `\n=== Errors\n${errors.join(os.EOL)}` : ''; 63 | throw new SfError(`${err}${combinedErrors}`, 'UNINSTALL_ERROR', [messages.getMessage('uninstallErrorAction')]); 64 | } 65 | } 66 | }; 67 | const pollingClient = await PollingClient.create({ 68 | poll: () => poll(uninstallRequestId), 69 | frequency, 70 | timeout: wait, 71 | }); 72 | return pollingClient.subscribe(); 73 | } 74 | 75 | export async function uninstallPackage( 76 | id: string, 77 | conn: Connection, 78 | frequency: Duration = Duration.seconds(0), 79 | wait: Duration = Duration.seconds(0) 80 | ): Promise { 81 | try { 82 | const uninstallRequest = await conn.tooling.sobject('SubscriberPackageVersionUninstallRequest').create({ 83 | SubscriberPackageVersionId: id, 84 | }); 85 | 86 | if (uninstallRequest.success) { 87 | if (wait.seconds === 0) { 88 | return (await conn.tooling 89 | .sobject('SubscriberPackageVersionUninstallRequest') 90 | .retrieve(uninstallRequest.id)) as UninstallResult; 91 | } else { 92 | return await pollUninstall(uninstallRequest.id, conn, frequency, wait); 93 | } 94 | } 95 | throw combineSaveErrors('SubscriberPackageVersionUninstallRequest', 'create', uninstallRequest.errors); 96 | } catch (err) { 97 | if (err instanceof Error) { 98 | throw applyErrorAction(massageErrorMessage(err)); 99 | } 100 | throw err; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salesforce/packaging", 3 | "version": "4.18.7", 4 | "description": "Packaging library for the Salesforce packaging platform", 5 | "main": "lib/exported", 6 | "types": "lib/exported.d.ts", 7 | "license": "Apache-2.0", 8 | "repository": "forcedotcom/packaging", 9 | "scripts": { 10 | "build": "wireit", 11 | "clean": "sf-clean", 12 | "clean-all": "sf-clean all", 13 | "compile": "wireit", 14 | "docs": "sf-docs", 15 | "fix-license": "eslint src test --fix --rule \"header/header: [2]\"", 16 | "format": "wireit", 17 | "link-check": "wireit", 18 | "lint": "wireit", 19 | "lint-fix": "yarn sf-lint --fix", 20 | "postcompile": "tsc -p test", 21 | "prepack": "sf-prepack", 22 | "prepare": "sf-install", 23 | "repl": "node --inspect ./scripts/repl.js", 24 | "test": "wireit", 25 | "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 1800000 --parallel --jobs 20", 26 | "test:only": "wireit" 27 | }, 28 | "keywords": [ 29 | "force", 30 | "salesforce", 31 | "sfdx", 32 | "salesforcedx", 33 | "packaging" 34 | ], 35 | "engines": { 36 | "node": ">=18.0.0" 37 | }, 38 | "files": [ 39 | "docs", 40 | "lib", 41 | "messages", 42 | "!lib/**/*.map" 43 | ], 44 | "dependencies": { 45 | "@jsforce/jsforce-node": "^3.10.10", 46 | "@salesforce/core": "^8.24.0", 47 | "@salesforce/kit": "^3.2.4", 48 | "@salesforce/schemas": "^1.10.3", 49 | "@salesforce/source-deploy-retrieve": "^12.31.0", 50 | "@salesforce/ts-types": "^2.0.12", 51 | "@salesforce/types": "^1.6.0", 52 | "fast-xml-parser": "^4.5.0", 53 | "globby": "^11", 54 | "graphology": "^0.26.0", 55 | "graphology-traversal": "^0.3.1", 56 | "graphology-types": "^0.24.7", 57 | "jszip": "^3.10.1", 58 | "object-treeify": "^2" 59 | }, 60 | "devDependencies": { 61 | "@salesforce/cli-plugins-testkit": "^5.3.41", 62 | "@salesforce/dev-scripts": "^11.0.4", 63 | "@types/globby": "^9.1.0", 64 | "@types/jszip": "^3.4.1", 65 | "eslint-plugin-sf-plugin": "^1.20.33", 66 | "ts-node": "^10.9.2", 67 | "typescript": "^5.9.3" 68 | }, 69 | "resolutions": { 70 | "@jsforce/jsforce-node/node-fetch/whatwg-url": "^14.0.0", 71 | "string-width": "^4.2.3" 72 | }, 73 | "publishConfig": { 74 | "access": "public" 75 | }, 76 | "wireit": { 77 | "build": { 78 | "dependencies": [ 79 | "compile", 80 | "lint" 81 | ] 82 | }, 83 | "compile": { 84 | "command": "tsc -p . --pretty --incremental", 85 | "files": [ 86 | "src/**/*.ts", 87 | "**/tsconfig.json", 88 | "messages/**" 89 | ], 90 | "output": [ 91 | "lib/**", 92 | "*.tsbuildinfo" 93 | ], 94 | "clean": "if-file-deleted" 95 | }, 96 | "format": { 97 | "command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\"", 98 | "files": [ 99 | "src/**/*.ts", 100 | "test/**/*.ts", 101 | "schemas/**/*.json", 102 | "command-snapshot.json", 103 | ".prettier*" 104 | ], 105 | "output": [] 106 | }, 107 | "lint": { 108 | "command": "eslint src test --color --cache --cache-location .eslintcache", 109 | "files": [ 110 | "src/**/*.ts", 111 | "test/**/*.ts", 112 | "messages/**", 113 | "**/.eslint*", 114 | "**/tsconfig.json" 115 | ], 116 | "output": [] 117 | }, 118 | "test:compile": { 119 | "command": "tsc -p \"./test\" --pretty", 120 | "files": [ 121 | "test/**/*.ts", 122 | "**/tsconfig.json" 123 | ], 124 | "output": [] 125 | }, 126 | "test": { 127 | "dependencies": [ 128 | "test:only", 129 | "test:compile", 130 | "link-check" 131 | ] 132 | }, 133 | "test:only": { 134 | "command": "nyc mocha \"test/**/*.test.ts\"", 135 | "env": { 136 | "FORCE_COLOR": "2" 137 | }, 138 | "files": [ 139 | "test/**/*.ts", 140 | "src/**/*.ts", 141 | "**/tsconfig.json", 142 | ".mocha*", 143 | "!*.nut.ts", 144 | ".nycrc" 145 | ], 146 | "output": [] 147 | }, 148 | "link-check": { 149 | "command": "node -e \"process.exit(process.env.CI ? 0 : 1)\" || linkinator \"**/*.md\" --skip \"CHANGELOG.md|node_modules|test/|confluence.internal.salesforce.com|my.salesforce.com|localhost|%s\" --markdown --retry --directory-listing --verbosity error", 150 | "files": [ 151 | "./*.md", 152 | "./!(CHANGELOG).md", 153 | "messages/**/*.md" 154 | ], 155 | "output": [] 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/package/versionNumber.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Messages } from '@salesforce/core'; 18 | 19 | Messages.importMessagesDirectory(__dirname); 20 | const messages = Messages.loadMessages('@salesforce/packaging', 'version_number'); 21 | 22 | export enum BuildNumberToken { 23 | LATEST_BUILD_NUMBER_TOKEN = 'LATEST', 24 | NEXT_BUILD_NUMBER_TOKEN = 'NEXT', 25 | RELEASED_BUILD_NUMBER_TOKEN = 'RELEASED', 26 | HIGHEST_VERSION_NUMBER_TOKEN = 'HIGHEST', 27 | NONE_VERSION_NUMBER_TOKEN = 'NONE', 28 | } 29 | 30 | export class VersionNumber { 31 | public constructor( 32 | public major: string | number, 33 | public minor: string | number, 34 | public patch: string | number, 35 | public build: string | number 36 | ) {} 37 | 38 | /** 39 | * Separates at major.minor string into {major: Number, minor: Number} object 40 | * 41 | * @param versionString a string in the format of major.minor like '3.2' 42 | */ 43 | public static parseMajorMinor(versionString: string): { major: number | null; minor: number | null } { 44 | const versions = versionString?.split('.'); 45 | if (!versions) { 46 | // return nulls so when no version option is provided, the server can infer the correct version 47 | return { major: null, minor: null }; 48 | } 49 | 50 | if (versions.length === 2) { 51 | return { 52 | major: Number(versions[0]), 53 | minor: Number(versions[1]), 54 | }; 55 | } else { 56 | throw messages.createError('invalidMajorMinorFormat', [versionString]); 57 | } 58 | } 59 | 60 | public static from(versionString: string | undefined): VersionNumber { 61 | if (!versionString) { 62 | throw messages.createError('errorMissingVersionNumber'); 63 | } 64 | const version = versionString.split('.'); 65 | if (version?.length === 4) { 66 | const [major, minor, patch, build] = version; 67 | const asNumbers = [major, minor, patch, build].map((v) => parseInt(v, 10)); 68 | if (asNumbers.slice(0, 3).some((v) => isNaN(v))) { 69 | throw messages.createError('errorInvalidMajorMinorPatchNumber', [versionString]); 70 | } 71 | if (isNaN(asNumbers[3]) && !VersionNumber.isABuildKeyword(build)) { 72 | throw messages.createError('errorInvalidBuildNumberToken', [ 73 | versionString, 74 | Object.values(BuildNumberToken).join(', '), 75 | ]); 76 | } 77 | return new VersionNumber(major, minor, patch, build); 78 | } 79 | throw messages.createError('errorInvalidVersionNumber', [versionString]); 80 | } 81 | 82 | public static isABuildKeyword(token: string | number): boolean { 83 | const buildNumberTokenValues = Object.values(BuildNumberToken); 84 | const results = buildNumberTokenValues.includes(token as BuildNumberToken); 85 | return results; 86 | } 87 | 88 | public toString(): string { 89 | { 90 | return `${this.major || '0'}.${this.minor || '0'}.${this.patch || '0'}.${this.build ? `${this.build}` : '0'}`; 91 | } 92 | } 93 | 94 | public isBuildKeyword(): boolean { 95 | return VersionNumber.isABuildKeyword(this.build); 96 | } 97 | 98 | public compareTo(other: VersionNumber): number { 99 | const [aMajor, aMinor, aPatch, aBuild] = [this.major, this.minor, this.patch, this.build].map((v) => 100 | typeof v === 'number' ? v : parseInt(v, 10) 101 | ); 102 | const [oMajor, oMinor, oPatch, oBuild] = [other.major, other.minor, other.patch, other.build].map((v) => 103 | typeof v === 'number' ? v : parseInt(v, 10) 104 | ); 105 | if (aMajor !== oMajor) { 106 | return aMajor - oMajor; 107 | } 108 | if (aMinor !== oMinor) { 109 | return aMinor - oMinor; 110 | } 111 | if (aPatch !== oPatch) { 112 | return aPatch - oPatch; 113 | } 114 | if (isNaN(aBuild) && isNaN(oBuild)) { 115 | return 0; 116 | } 117 | if (isNaN(aBuild)) { 118 | return 1; 119 | } 120 | if (isNaN(oBuild)) { 121 | return -1; 122 | } 123 | if (aBuild !== oBuild) { 124 | return aBuild - oBuild; 125 | } 126 | return 0; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/interfaces/bundleSObjects.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Nullable } from '@salesforce/ts-types'; 17 | import { BundleEntry } from '@salesforce/schemas'; 18 | import type { Schema } from '@jsforce/jsforce-node'; 19 | 20 | export { BundleEntry }; 21 | 22 | export type QueryRecord = Schema & { 23 | Id: string; 24 | PackageBundle?: { 25 | Id: string; 26 | BundleName: string; 27 | Description?: string; 28 | IsDeleted: boolean; 29 | CreatedDate: string; 30 | CreatedById: string; 31 | LastModifiedDate: string; 32 | LastModifiedById: string; 33 | SystemModstamp: string; 34 | }; 35 | VersionName: string; 36 | MajorVersion: string; 37 | MinorVersion: string; 38 | IsReleased: boolean; 39 | Ancestor?: { 40 | Id: string; 41 | PackageBundle?: { 42 | Id: string; 43 | BundleName: string; 44 | Description?: string; 45 | IsDeleted: boolean; 46 | CreatedDate: string; 47 | CreatedById: string; 48 | LastModifiedDate: string; 49 | LastModifiedById: string; 50 | SystemModstamp: string; 51 | }; 52 | VersionName: string; 53 | MajorVersion: string; 54 | MinorVersion: string; 55 | IsReleased: boolean; 56 | }; 57 | }; 58 | 59 | export type AncestorRecord = { 60 | Id: string; 61 | PackageBundle?: { 62 | Id: string; 63 | BundleName: string; 64 | Description?: string; 65 | IsDeleted: boolean; 66 | CreatedDate: string; 67 | CreatedById: string; 68 | LastModifiedDate: string; 69 | LastModifiedById: string; 70 | SystemModstamp: string; 71 | }; 72 | VersionName: string; 73 | MajorVersion: string; 74 | MinorVersion: string; 75 | IsReleased: boolean; 76 | }; 77 | 78 | export namespace BundleSObjects { 79 | export type Bundle = { 80 | BundleName: string; 81 | Description?: string; 82 | Id: string; 83 | IsDeleted: boolean; 84 | CreatedDate: string; 85 | CreatedById: string; 86 | LastModifiedDate: string; 87 | LastModifiedById: string; 88 | SystemModstamp: string; 89 | }; 90 | 91 | export type BundleVersion = { 92 | Id: string; 93 | PackageBundle: Bundle; 94 | VersionName: string; 95 | MajorVersion: string; 96 | MinorVersion: string; 97 | Ancestor: Nullable; 98 | IsReleased: boolean; 99 | CreatedDate: string; 100 | CreatedById: string; 101 | LastModifiedDate: string; 102 | LastModifiedById: string; 103 | }; 104 | 105 | export type PkgBundleVersionCreateReq = { 106 | PackageBundleId: string; 107 | VersionName: string; 108 | MajorVersion: string; 109 | MinorVersion: string; 110 | BundleVersionComponents: string; 111 | Ancestor?: string | null; 112 | }; 113 | 114 | export type PackageBundleVersionCreateRequestResult = PkgBundleVersionCreateReq & { 115 | Id: string; 116 | PackageBundleVersionId: string; 117 | RequestStatus: PkgBundleVersionCreateReqStatus; 118 | CreatedDate: string; 119 | CreatedById: string; 120 | Error?: string[]; 121 | ValidationError?: string; 122 | }; 123 | 124 | export enum PkgBundleVersionCreateReqStatus { 125 | queued = 'Queued', 126 | inProgress = 'InProgress', 127 | success = 'Success', 128 | error = 'Error', 129 | } 130 | 131 | export enum PkgBundleVersionInstallReqStatus { 132 | queued = 'Queued', 133 | inProgress = 'InProgress', 134 | success = 'Success', 135 | error = 'Error', 136 | } 137 | 138 | export type PkgBundleVersionQueryRecord = { 139 | Id: string; 140 | RequestStatus: BundleSObjects.PkgBundleVersionCreateReqStatus; 141 | PackageBundle: Bundle; 142 | PackageBundleVersion: BundleVersion; 143 | VersionName: string; 144 | MajorVersion: string; 145 | MinorVersion: string; 146 | Ancestor: BundleVersion; 147 | BundleVersionComponents: string; 148 | CreatedDate: string; 149 | CreatedById: string; 150 | Error?: string[]; 151 | ValidationError?: string; 152 | } & Schema; 153 | 154 | export type PkgBundleVersionInstallReq = { 155 | PackageBundleVersionId: string; 156 | DevelopmentOrganization: string; 157 | }; 158 | 159 | export type PkgBundleVersionInstallReqResult = PkgBundleVersionInstallReq & { 160 | Id: string; 161 | InstallStatus: PkgBundleVersionInstallReqStatus; 162 | ValidationError: string; 163 | CreatedDate: string; 164 | CreatedById: string; 165 | Error?: string[]; 166 | }; 167 | export type PkgBundleVersionInstallQueryRecord = { 168 | Id: string; 169 | InstallStatus: BundleSObjects.PkgBundleVersionInstallReqStatus; 170 | PackageBundleVersionId: string; 171 | DevelopmentOrganization: string; 172 | ValidationError: string; 173 | CreatedDate: string; 174 | CreatedById: string; 175 | Error?: string[]; 176 | } & Schema; 177 | } 178 | -------------------------------------------------------------------------------- /src/package/packageVersionReport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Node 18 | import util from 'node:util'; 19 | 20 | // Local 21 | import { Connection, Logger, SfProject } from '@salesforce/core'; 22 | import * as pkgUtils from '../utils/packageUtils'; 23 | import { PackageVersionReportResult } from '../interfaces'; 24 | 25 | const defaultFields = [ 26 | 'Id', 27 | 'Package2Id', 28 | 'SubscriberPackageVersionId', 29 | 'Name', 30 | 'Description', 31 | 'Tag', 32 | 'Branch', 33 | 'AncestorId', 34 | 'ValidationSkipped', 35 | 'MajorVersion', 36 | 'MinorVersion', 37 | 'PatchVersion', 38 | 'BuildNumber', 39 | 'IsReleased', 40 | 'CodeCoverage', 41 | 'HasPassedCodeCoverageCheck', 42 | 'Package2.IsOrgDependent', 43 | 'ReleaseVersion', 44 | 'BuildDurationInSeconds', 45 | 'HasMetadataRemoved', 46 | 'CreatedById', 47 | 'ConvertedFromVersionId', 48 | ]; 49 | 50 | let verboseFields = ['SubscriberPackageVersion.Dependencies', 'CodeCoveragePercentages']; 51 | 52 | // Ensure we only include the async validation property for api version of v60.0 or higher. 53 | const default61Fields = ['ValidatedAsync']; 54 | 55 | // Add fields here that are available only api version of v64.0 or higher. 56 | const default64Fields = ['TotalNumberOfMetadataFiles', 'TotalSizeOfMetadataFiles']; 57 | 58 | const verbose61Fields = ['EndToEndBuildDurationInSeconds']; 59 | 60 | const DEFAULT_ORDER_BY_FIELDS = 'Package2Id, Branch, MajorVersion, MinorVersion, PatchVersion, BuildNumber'; 61 | 62 | let logger: Logger; 63 | const getLogger = (): Logger => { 64 | if (!logger) { 65 | logger = Logger.childFromRoot('getPackageVersionReport'); 66 | } 67 | return logger; 68 | }; 69 | 70 | function constructQuery(connectionVersion: number, verbose: boolean): string { 71 | // Ensure we only include the async validation property for api version of v60.0 or higher. 72 | // TotalNumberOfMetadataFiles is included as query field for api version of v64.0 or higher. 73 | let queryFields = 74 | connectionVersion > 63 75 | ? [...defaultFields, ...default61Fields, ...default64Fields] 76 | : connectionVersion > 60 77 | ? [...defaultFields, ...default61Fields] 78 | : defaultFields; 79 | verboseFields = connectionVersion > 60 ? [...verboseFields, ...verbose61Fields] : verboseFields; 80 | if (verbose) { 81 | queryFields = [...queryFields, ...verboseFields]; 82 | } 83 | const select = `SELECT ${queryFields.toString()} FROM Package2Version`; 84 | const wherePart = "WHERE Id = '%s' AND IsDeprecated != true"; 85 | const orderByPart = `ORDER BY ${DEFAULT_ORDER_BY_FIELDS}`; 86 | 87 | const query = `${select} ${wherePart} ${orderByPart}`; 88 | getLogger().debug(query); 89 | return query; 90 | } 91 | 92 | export async function getPackageVersionReport(options: { 93 | packageVersionId: string; 94 | connection: Connection; 95 | project?: SfProject; 96 | verbose: boolean; 97 | }): Promise { 98 | getLogger().debug(`entering getPackageVersionReport(${util.inspect(options, { depth: null })})`); 99 | const queryResult = await options.connection.tooling.query( 100 | util.format(constructQuery(Number(options.connection.version), options.verbose), options.packageVersionId) 101 | ); 102 | const records = queryResult.records; 103 | if (records?.length > 0) { 104 | const record = records[0]; 105 | record.Version = [record.MajorVersion, record.MinorVersion, record.PatchVersion, record.BuildNumber].join('.'); 106 | 107 | const containerOptions = await pkgUtils.getContainerOptions(record.Package2Id, options.connection); 108 | if (containerOptions.size > 0 && record.Package2Id) { 109 | record.PackageType = containerOptions.get(record.Package2Id); 110 | } 111 | 112 | record.AncestorVersion = null; 113 | 114 | if (record.AncestorId) { 115 | // lookup AncestorVersion value 116 | const ancestorVersionMap = await pkgUtils.getPackageVersionStrings([record.AncestorId], options.connection); 117 | record.AncestorVersion = ancestorVersionMap.get(record.AncestorId); 118 | } else if (record.PackageType !== 'Managed') { 119 | record.AncestorVersion = null; 120 | record.AncestorId = null; 121 | } 122 | 123 | record.HasPassedCodeCoverageCheck = 124 | record.Package2.IsOrgDependent === true || record.ValidationSkipped === true 125 | ? null 126 | : record.HasPassedCodeCoverageCheck; 127 | 128 | record.Package2.IsOrgDependent = record.PackageType === 'Managed' ? null : !!record.Package2.IsOrgDependent; 129 | 130 | // set HasMetadataRemoved to null Unlocked, otherwise use existing value 131 | record.HasMetadataRemoved = record.PackageType !== 'Managed' ? null : !!record.HasMetadataRemoved; 132 | 133 | return records; 134 | } 135 | return []; 136 | } 137 | -------------------------------------------------------------------------------- /test/package/packageVersion.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import { instantiateContext, MockTestOrgData, restoreContext, stubContext } from '@salesforce/core/testSetup'; 19 | import { expect } from 'chai'; 20 | import { Connection, SfError, SfProject } from '@salesforce/core'; 21 | 22 | import { PackageVersion } from '../../src/package'; 23 | import { PackageVersionCreate } from '../../src/package/packageVersionCreate'; 24 | 25 | describe('Package Version', () => { 26 | const $$ = instantiateContext(); 27 | const testOrg = new MockTestOrgData(); 28 | const packageId = '0Ho3i000000Gmj6XXX'; 29 | const uniquePackageId = '0Ho3i000000Gmj7XXX'; 30 | const idOrAlias = '04t4p000001ztuFAAQ'; 31 | const versionCreateRequestId = '08c5d00000blah'; 32 | let connection: Connection; 33 | let project: SfProject; 34 | let packageVersion: PackageVersion; 35 | 36 | beforeEach(async () => { 37 | $$.inProject(true); 38 | project = SfProject.getInstance(); 39 | project.getSfProjectJson().set('packageDirectories', [ 40 | { 41 | path: 'pkg', 42 | package: 'dep', 43 | versionName: 'ver 0.1', 44 | versionNumber: '0.1.0.NEXT', 45 | default: false, 46 | }, 47 | { 48 | path: 'force-app', 49 | package: 'TEST', 50 | versionName: 'ver 0.1', 51 | versionNumber: '0.1.0.NEXT', 52 | default: true, 53 | ancestorId: 'TEST2', 54 | unpackagedMetadata: { 55 | path: 'unpackaged', 56 | }, 57 | dependencies: [ 58 | { 59 | package: 'DEP@0.1.0-1', 60 | }, 61 | ], 62 | }, 63 | ]); 64 | project.getSfProjectJson().set('packageAliases', { 65 | dupPkg1: packageId, 66 | dupPkg2: packageId, 67 | uniquePkg: uniquePackageId, 68 | }); 69 | await project.getSfProjectJson().write(); 70 | 71 | await fs.promises.mkdir(path.join(project.getPath(), 'force-app')); 72 | stubContext($$); 73 | await $$.stubAuths(testOrg); 74 | connection = await testOrg.getConnection(); 75 | $$.SANDBOX.stub(connection.tooling, 'query') 76 | .onFirstCall() // @ts-ignore 77 | .resolves({ 78 | records: [ 79 | { 80 | Branch: null, 81 | MajorVersion: '1', 82 | MinorVersion: '2', 83 | PatchVersion: '3', 84 | }, 85 | ], 86 | }); 87 | }); 88 | 89 | beforeEach(() => { 90 | packageVersion = new PackageVersion({ connection, project, idOrAlias }); 91 | }); 92 | 93 | afterEach(async () => { 94 | restoreContext($$); 95 | await fs.promises.rmdir(path.join(project.getPath(), 'force-app')); 96 | // @ts-ignore 97 | project.packageDirectories = undefined; 98 | }); 99 | 100 | describe('updateProjectWithPackageVersion', () => { 101 | it('should save alias for the first duplicate 0Ho in aliases', async () => { 102 | // @ts-ignore 103 | await packageVersion.updateProjectWithPackageVersion({ 104 | Package2Id: uniquePackageId, 105 | SubscriberPackageVersionId: idOrAlias, 106 | }); 107 | expect(project.getSfProjectJson().getPackageAliases()?.['uniquePkg@1.2.3']).to.equal(idOrAlias); 108 | }); 109 | 110 | it('should save alias for unique 0Ho in aliases', async () => { 111 | // @ts-ignore 112 | await packageVersion.updateProjectWithPackageVersion({ 113 | Package2Id: packageId, 114 | SubscriberPackageVersionId: idOrAlias, 115 | }); 116 | expect(project.getSfProjectJson().getPackageAliases()?.['dupPkg1@1.2.3']).to.equal(idOrAlias); 117 | }); 118 | }); 119 | 120 | describe('create', () => { 121 | it('should include the package version create request ID', async () => { 122 | // @ts-expect-error partial mock 123 | $$.SANDBOX.stub(PackageVersionCreate.prototype, 'createPackageVersion').resolves({ 124 | Id: versionCreateRequestId, 125 | }); 126 | const pollingTimeoutError = new SfError('polling timed out', 'PollingClientTimeout'); 127 | $$.SANDBOX.stub(PackageVersion, 'pollCreateStatus').rejects(pollingTimeoutError); 128 | 129 | try { 130 | await PackageVersion.create({ connection, project }); 131 | expect(false).to.equal(true, 'Expected a PollingClientTimeout to be thrown'); 132 | } catch (err) { 133 | expect(err).to.be.instanceOf(SfError); 134 | expect(err).to.have.property('name', 'PollingClientTimeout'); 135 | expect(err).to.have.deep.property('data', { VersionCreateRequestId: versionCreateRequestId }); 136 | expect(err) 137 | .to.have.property('message') 138 | .and.include(`Run 'sf package version create report -i ${versionCreateRequestId}`); 139 | } 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | * Using welcoming and inclusive language 39 | * Being respectful of differing viewpoints and experiences 40 | * Gracefully accepting constructive criticism 41 | * Focusing on what is best for the community 42 | * Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | * The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | * Personal attacks, insulting/derogatory comments, or trolling 49 | * Public or private harassment 50 | * Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | * Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | * Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org (https://www.contributor-covenant.org/) 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /test/package1/package1VersionCreate.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import os from 'node:os'; 17 | import { instantiateContext, MockTestOrgData, restoreContext, stubContext } from '@salesforce/core/testSetup'; 18 | import { Duration } from '@salesforce/kit'; 19 | import { expect } from 'chai'; 20 | import { Connection, Lifecycle } from '@salesforce/core'; 21 | import { assert } from 'sinon'; 22 | import { PackageVersionEvents, PackagingSObjects } from '../../src/interfaces'; 23 | import { Package1Version } from '../../src/package1'; 24 | 25 | const options = { 26 | MetadataPackageId: '0334p000000blVyGAI', 27 | VersionName: 'Test', 28 | Description: 'Test', 29 | MajorVersion: 0, 30 | MinorVersion: 0, 31 | IsReleaseVersion: false, 32 | ReleaseNotesUrl: 'Test', 33 | PostInstallUrl: 'Test', 34 | Password: 'Test', 35 | }; 36 | 37 | const successResult = { 38 | Status: 'SUCCESS', 39 | Id: '0HD4p000000blVyGAI', 40 | MetadataPackageVersionId: '04t4p000002Bb4lAAC', 41 | MetadataPackageId: '03346000000MrC0AAK', 42 | }; 43 | 44 | const queuedResult = { 45 | Status: 'QUEUED', 46 | Id: '0HD4p000000blVyGAI', 47 | MetadataPackageVersionId: '04t4p000002Bb4lAAC', 48 | MetadataPackageId: '03346000000MrC0AAK', 49 | }; 50 | 51 | describe('Package1 Version Create', () => { 52 | const $$ = instantiateContext(); 53 | const testOrg = new MockTestOrgData(); 54 | let conn: Connection; 55 | let sobjectStub: sinon.SinonStub; 56 | 57 | beforeEach(async () => { 58 | stubContext($$); 59 | await $$.stubAuths(testOrg); 60 | conn = await testOrg.getConnection(); 61 | sobjectStub = $$.SANDBOX.stub(conn.tooling, 'sobject') 62 | .onFirstCall() 63 | .returns({ 64 | // @ts-ignore - to avoid stubbing every property of sobject 65 | create: () => ({ id: '0HD4p000000blUvGXX', success: true, errors: [] }), 66 | }); 67 | }); 68 | 69 | afterEach(() => { 70 | restoreContext($$); 71 | }); 72 | 73 | it('should send the create request, wait for it to finish, and emit events along the way', async () => { 74 | sobjectStub 75 | .onSecondCall() 76 | .returns({ 77 | retrieve: async () => queuedResult, 78 | }) 79 | .onThirdCall() 80 | .returns({ 81 | retrieve: async () => successResult, 82 | }); 83 | Lifecycle.getInstance().on( 84 | PackageVersionEvents.create.progress, 85 | async (data: { timeout: number; pollingResult: PackagingSObjects.PackageUploadRequest }) => { 86 | // 3 minute timeout (180 seconds) - 1 second per poll 87 | expect(data.timeout).to.equal(179); 88 | expect(data.pollingResult.Status).to.equal('QUEUED'); 89 | } 90 | ); 91 | const result = await Package1Version.create(conn, options, { 92 | frequency: Duration.seconds(1), 93 | timeout: Duration.minutes(3), 94 | }); 95 | expect(result).deep.equal(successResult); 96 | }); 97 | 98 | it('should send the create request, and handle errors appropriately', async () => { 99 | sobjectStub.onSecondCall().returns({ 100 | retrieve: async () => ({ 101 | Status: 'ERROR', 102 | Errors: { errors: [new Error('message 1'), new Error('message 2')] }, 103 | }), 104 | }); 105 | 106 | try { 107 | await Package1Version.create(conn, options, { frequency: Duration.seconds(1), timeout: Duration.minutes(3) }); 108 | assert.fail('the above should throw an error from polling'); 109 | } catch (e) { 110 | expect((e as Error).message).to.equal(`Package upload failed.${os.EOL}message 1${os.EOL}message 2`); 111 | } 112 | }); 113 | 114 | it('should send the create request, and handle errors appropriately (0 error messages)', async () => { 115 | sobjectStub.onSecondCall().returns({ 116 | retrieve: async () => ({ 117 | Status: 'ERROR', 118 | Errors: [], 119 | }), 120 | }); 121 | Lifecycle.getInstance().on( 122 | PackageVersionEvents.create.progress, 123 | async (data: { timeout: number; pollingResult: PackagingSObjects.PackageUploadRequest }) => { 124 | // 3 minute timeout (180 seconds) - 1 second per poll 125 | expect(data.timeout).to.equal(179); 126 | } 127 | ); 128 | try { 129 | await Package1Version.create(conn, options, { 130 | frequency: Duration.seconds(1), 131 | timeout: Duration.minutes(3), 132 | }); 133 | assert.fail('the above should throw an error from polling'); 134 | } catch (e) { 135 | expect((e as Error).message).to.equal('Package version creation failed with unknown error'); 136 | } 137 | }); 138 | 139 | it('should send the create request, retrieve the request and return', async () => { 140 | sobjectStub.onSecondCall().returns({ 141 | retrieve: async () => queuedResult, 142 | }); 143 | 144 | const result = await Package1Version.create(conn, options); 145 | expect(result).deep.equal(queuedResult); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/package/packageVersionList.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { Connection, Logger, Messages } from '@salesforce/core'; 18 | import { QueryResult, Schema } from '@jsforce/jsforce-node'; 19 | import { isNumber } from '@salesforce/ts-types'; 20 | import { BY_LABEL, validateId } from '../utils/packageUtils'; 21 | import { PackageVersionListOptions, PackageVersionListResult } from '../interfaces'; 22 | 23 | Messages.importMessagesDirectory(__dirname); 24 | const messages = Messages.loadMessages('@salesforce/packaging', 'package_version_create'); 25 | 26 | const defaultFields = [ 27 | 'Id', 28 | 'Package2Id', 29 | 'SubscriberPackageVersionId', 30 | 'Name', 31 | 'Package2.Name', 32 | 'Package2.NamespacePrefix', 33 | 'Package2.IsOrgDependent', 34 | 'Description', 35 | 'Tag', 36 | 'Branch', 37 | 'MajorVersion', 38 | 'MinorVersion', 39 | 'PatchVersion', 40 | 'BuildNumber', 41 | 'IsReleased', 42 | 'CreatedDate', 43 | 'LastModifiedDate', 44 | 'IsPasswordProtected', 45 | 'AncestorId', 46 | 'ValidationSkipped', 47 | 'CreatedById', 48 | 'ConvertedFromVersionId', 49 | 'ReleaseVersion', 50 | 'BuildDurationInSeconds', 51 | 'HasMetadataRemoved', 52 | ]; 53 | 54 | const verboseFields = ['CodeCoverage', 'HasPassedCodeCoverageCheck']; 55 | 56 | const verbose57Fields = ['Language']; 57 | 58 | // Ensure we only include the async validation property for api version of v60.0 or higher. 59 | const default61Fields = ['ValidatedAsync']; 60 | 61 | export const DEFAULT_ORDER_BY_FIELDS = 'Package2Id, Branch, MajorVersion, MinorVersion, PatchVersion, BuildNumber'; 62 | 63 | let logger: Logger; 64 | const getLogger = (): Logger => { 65 | if (!logger) { 66 | logger = Logger.childFromRoot('packageVersionList'); 67 | } 68 | return logger; 69 | }; 70 | 71 | /** 72 | * Returns all the package versions that are available in the org, up to 10,000. 73 | * If more records are needed use the `SF_ORG_MAX_QUERY_LIMIT` env var. 74 | * 75 | * @param connection 76 | * @param options (optional) PackageVersionListOptions 77 | */ 78 | export async function listPackageVersions( 79 | connection: Connection, 80 | options?: PackageVersionListOptions 81 | ): Promise> { 82 | const query = constructQuery(Number(connection.version), options); 83 | return connection.autoFetchQuery(query, { tooling: true }); 84 | } 85 | 86 | export function constructQuery(connectionVersion: number, options?: PackageVersionListOptions): string { 87 | // construct custom WHERE clause, if applicable 88 | const where = constructWhere(options); 89 | let queryFields = connectionVersion > 60 ? [...defaultFields, ...default61Fields] : defaultFields; 90 | if (options?.verbose) { 91 | queryFields = [...queryFields, ...verboseFields]; 92 | if (connectionVersion >= 57) { 93 | queryFields = [...queryFields, ...verbose57Fields]; 94 | } 95 | } 96 | const query = `SELECT ${queryFields.toString()} FROM Package2Version`; 97 | 98 | return assembleQueryParts(query, where, options?.orderBy); 99 | } 100 | 101 | export function assembleQueryParts(select: string, where: string[], orderBy?: string): string { 102 | // construct ORDER BY clause 103 | const orderByPart = `ORDER BY ${orderBy ? orderBy : DEFAULT_ORDER_BY_FIELDS}`; 104 | const wherePart = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; 105 | 106 | const query = `${select} ${wherePart} ${orderByPart}`; 107 | getLogger().debug(query); 108 | return query; 109 | } 110 | 111 | // construct custom WHERE clause parts 112 | export function constructWhere(options?: PackageVersionListOptions): string[] { 113 | const where: string[] = []; 114 | 115 | // filter on given package ids 116 | if (options?.packages?.length) { 117 | // remove dups 118 | const uniquePackageIds = [...new Set(options?.packages)]; 119 | 120 | // validate ids 121 | uniquePackageIds.forEach((packageId) => { 122 | validateId(BY_LABEL.PACKAGE_ID, packageId); 123 | }); 124 | 125 | // stash where part 126 | where.push(`Package2Id IN ('${uniquePackageIds.join("','")}')`); 127 | } 128 | 129 | // filter on created date, days ago: 0 for today, etc 130 | if (isNumber(options?.createdLastDays)) { 131 | const createdLastDays = validateDays('createdlastdays', options?.createdLastDays); 132 | where.push(`CreatedDate = LAST_N_DAYS:${createdLastDays}`); 133 | } 134 | 135 | // filter on last mod date, days ago: 0 for today, etc 136 | if (isNumber(options?.modifiedLastDays)) { 137 | const modifiedLastDays = validateDays('modifiedlastdays', options?.modifiedLastDays); 138 | where.push(`LastModifiedDate = LAST_N_DAYS:${modifiedLastDays}`); 139 | } 140 | 141 | if (options?.isReleased) { 142 | where.push('IsReleased = true'); 143 | } 144 | 145 | if (options?.showConversionsOnly) { 146 | where.push('ConvertedFromVersionId != null'); 147 | } 148 | 149 | if (options?.branch) { 150 | where.push(`Branch='${options.branch}'`); 151 | } 152 | 153 | // exclude deleted 154 | where.push('IsDeprecated = false'); 155 | return where; 156 | } 157 | 158 | export function validateDays(paramName: string, lastDays = -1): number { 159 | if (lastDays < 0) { 160 | throw messages.createError('invalidDaysNumber', [paramName, lastDays]); 161 | } 162 | 163 | return lastDays; 164 | } 165 | -------------------------------------------------------------------------------- /test/package/uninstallPackage.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { instantiateContext, MockTestOrgData, restoreContext, stubContext } from '@salesforce/core/testSetup'; 17 | import { Duration } from '@salesforce/kit'; 18 | import { expect } from 'chai'; 19 | import { Connection, Lifecycle, SfError } from '@salesforce/core'; 20 | import { assert } from 'sinon'; 21 | import { PackageEvents, PackagingSObjects } from '../../src/interfaces'; 22 | import { uninstallPackage } from '../../src/package/packageUninstall'; 23 | 24 | const packageId = '04t4p000002BaHYXXX'; 25 | 26 | const successResult = { 27 | Id: '06y23000000002MXXX', 28 | IsDeleted: false, 29 | CreatedDate: '2022-08-02T17:13:00.000+0000', 30 | CreatedById: '00523000003Ehj9XXX', 31 | LastModifiedDate: '2022-08-02T17:13:00.000+0000', 32 | LastModifiedById: '00523000003Ehj9XXX', 33 | SystemModstamp: '2022-08-02T17:13:00.000+0000', 34 | SubscriberPackageVersionId: '04t4p000002BaHYXXX', 35 | Status: 'Success', 36 | }; 37 | 38 | const queuedResult = { ...successResult, Status: 'Queued' }; 39 | 40 | describe('Package Uninstall', () => { 41 | const $$ = instantiateContext(); 42 | const testOrg = new MockTestOrgData(); 43 | let conn: Connection; 44 | let sobjectStub: sinon.SinonStub; 45 | 46 | beforeEach(async () => { 47 | stubContext($$); 48 | await $$.stubAuths(testOrg); 49 | conn = await testOrg.getConnection(); 50 | sobjectStub = $$.SANDBOX.stub(conn.tooling, 'sobject') 51 | .onFirstCall() 52 | .returns({ 53 | // @ts-ignore - to avoid stubbing every property of sobject 54 | create: () => ({ id: '04t4p000002BaHYAA0', success: true, errors: [] }), 55 | }); 56 | }); 57 | 58 | afterEach(() => { 59 | restoreContext($$); 60 | }); 61 | 62 | it('should send the uninstall request, wait for it to finish, and emit events along the way', async () => { 63 | const millis1 = Duration.milliseconds(1); 64 | $$.SANDBOX.stub(Duration, 'seconds').callsFake(() => millis1); 65 | sobjectStub 66 | .onSecondCall() 67 | .returns({ 68 | retrieve: async () => queuedResult, 69 | }) 70 | .onThirdCall() 71 | .returns({ 72 | retrieve: async () => successResult, 73 | }); 74 | Lifecycle.getInstance().on( 75 | PackageEvents.uninstall, 76 | async (data: PackagingSObjects.SubscriberPackageVersionUninstallRequest) => { 77 | expect(data.Status).to.equal('Queued'); 78 | } 79 | ); 80 | 81 | const result = await uninstallPackage(packageId, conn, Duration.seconds(5), Duration.minutes(3)); 82 | expect(result).deep.equal(successResult); 83 | }); 84 | 85 | it('should send the uninstall request, and handle errors appropriately', async () => { 86 | sobjectStub.onSecondCall().returns({ 87 | retrieve: async () => ({ 88 | Id: '06y23000000002MXXX', 89 | Status: 'ERROR', 90 | Errors: { errors: [new Error('message 1'), new Error('message 2')] }, 91 | }), 92 | }); 93 | // @ts-ignore 94 | $$.SANDBOX.stub(conn.tooling, 'query').resolves({ 95 | records: [{ Message: 'this is a server-side error message' }, { Message: 'this is a second error message' }], 96 | }); 97 | 98 | try { 99 | await uninstallPackage(packageId, conn, Duration.seconds(5), Duration.minutes(3)); 100 | assert.fail('the above should throw an error from polling'); 101 | } catch (e) { 102 | const error = e as SfError; 103 | expect(error.message).to.include( 104 | "Can't uninstall the package 04t4p000002BaHYAA0 during uninstall request 06y23000000002MXXX." 105 | ); 106 | expect(error.message).to.include('=== Errors'); 107 | expect(error.message).to.include('(1) this is a server-side error message'); 108 | expect(error.message).to.include('(2) this is a second error message'); 109 | expect(error.actions).to.deep.equal(['Verify installed package ID and resolve errors, then try again.']); 110 | } 111 | }); 112 | 113 | it('should send the uninstall request, and handle errors appropriately (0 error messages)', async () => { 114 | sobjectStub.onSecondCall().returns({ 115 | retrieve: async () => ({ 116 | Id: '04t4p000002BaHYXXX', 117 | Status: 'ERROR', 118 | Errors: [], 119 | }), 120 | }); 121 | Lifecycle.getInstance().on( 122 | PackageEvents.uninstall, 123 | async (data: { timeout: number; pollingResult: PackagingSObjects.PackageUploadRequest }) => { 124 | // 3 minute timeout (180 seconds) - 1 second per poll 125 | expect(data.timeout).to.equal(179); 126 | } 127 | ); 128 | try { 129 | await uninstallPackage(packageId, conn, Duration.seconds(5), Duration.minutes(3)); 130 | assert.fail('the above should throw an error from polling'); 131 | } catch (e) { 132 | expect((e as SfError).message).to.equal( 133 | "Can't uninstall the package 04t4p000002BaHYAA0 during uninstall request 04t4p000002BaHYXXX." 134 | ); 135 | expect((e as SfError).actions).to.deep.equal(['Verify installed package ID and resolve errors, then try again.']); 136 | } 137 | }); 138 | 139 | it('should send the uninstall request, retrieve the status and return', async () => { 140 | sobjectStub.onSecondCall().returns({ 141 | retrieve: async () => queuedResult, 142 | }); 143 | 144 | const result = await uninstallPackage(packageId, conn, Duration.minutes(0), Duration.minutes(0)); 145 | expect(result).deep.equal(queuedResult); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/package/profileRewriter.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect, config } from 'chai'; 18 | import { 19 | profileRewriter, 20 | CorrectedProfile, 21 | fieldCorrections, 22 | profileObjectToString, 23 | profileStringToProfile, 24 | } from '../../src/package/profileRewriter'; 25 | 26 | config.truncateThreshold = 0; 27 | 28 | const sampleProfile: Partial = { 29 | userLicense: 'Salesforce', 30 | // something that should be removed when packaging profiles 31 | custom: true, 32 | pageAccesses: [ 33 | { 34 | apexPage: 'Foo', 35 | enabled: true, 36 | }, 37 | ], 38 | objectPermissions: [ 39 | { 40 | object: 'Foo__c', 41 | allowRead: true, 42 | }, 43 | { 44 | object: 'Bar__c', 45 | allowRead: true, 46 | }, 47 | // this one should be removed because it's not in the manifest 48 | { 49 | object: 'Baz__c', 50 | allowRead: true, 51 | }, 52 | ], 53 | fieldPermissions: [ 54 | { 55 | field: 'Foo__c.Foo_Field__c', 56 | readable: true, 57 | editable: true, 58 | }, 59 | { 60 | field: 'Event.Event_Field__c', 61 | readable: true, 62 | editable: true, 63 | }, 64 | { 65 | field: 'Task.Task_Field__c', 66 | readable: true, 67 | editable: true, 68 | }, 69 | // this one should be removed because it's not in the manifest 70 | { 71 | field: 'Foo__c.Omit__c', 72 | readable: true, 73 | editable: true, 74 | }, 75 | { 76 | field: 'Foo__c.Omit2__c', 77 | readable: true, 78 | editable: true, 79 | }, 80 | ], 81 | }; 82 | 83 | const sampleManifest = new Map([ 84 | ['CustomObject', ['Foo__c', 'Bar__c']], 85 | ['CustomField', ['Foo__c.Foo_Field__c', 'Activity.Event_Field__c', 'Activity.Task_Field__c']], 86 | ]); 87 | 88 | describe('reading and writing profiles', () => { 89 | it('reads a profile with single-item nodes', () => { 90 | const profileJson: Partial = { 91 | objectPermissions: [ 92 | { 93 | object: 'Foo__c', 94 | allowRead: true, 95 | }, 96 | ], 97 | }; 98 | const xml = profileObjectToString(profileJson); 99 | const json = profileStringToProfile(xml); 100 | expect(json.objectPermissions).to.deep.equal([{ object: 'Foo__c', allowRead: 'true' }]); 101 | }); 102 | it('writes include the outer object, xmlns and declaration', () => { 103 | const objectContents: Partial = { 104 | objectPermissions: [ 105 | { 106 | object: 'Foo__c', 107 | allowRead: true, 108 | }, 109 | ], 110 | }; 111 | const result = profileObjectToString(objectContents); 112 | expect(result).to.include(''); 113 | expect(result).to.include(''); 114 | }); 115 | }); 116 | 117 | describe('fieldCorrections', () => { 118 | it('event and task => activity', () => { 119 | expect(fieldCorrections('Event.Event_Field__c')).to.equal('Activity.Event_Field__c'); 120 | expect(fieldCorrections('Task.Task_Field__c')).to.equal('Activity.Task_Field__c'); 121 | }); 122 | it('does not change other fields', () => { 123 | expect(fieldCorrections('Foo__c.Foo_Field__c')).to.equal('Foo__c.Foo_Field__c'); 124 | }); 125 | }); 126 | 127 | describe('profileRewriter', () => { 128 | describe('user license', () => { 129 | it('retains userLicense when retainUserLicense is true', () => { 130 | expect(profileRewriter(sampleProfile as CorrectedProfile, sampleManifest, true)).to.have.property('userLicense'); 131 | }); 132 | it('omits userLicense when retainUserLicense is false', () => { 133 | expect(profileRewriter(sampleProfile as CorrectedProfile, sampleManifest, false)).to.not.have.property( 134 | 'userLicense' 135 | ); 136 | }); 137 | }); 138 | it('omits properties that are not in the metadata types used for packaging', () => { 139 | expect(profileRewriter(sampleProfile as CorrectedProfile, sampleManifest, false)).to.not.have.property('custom'); 140 | }); 141 | it('filters objectPermissions for Objects not in the manifest', () => { 142 | const newProfile = profileRewriter(sampleProfile as CorrectedProfile, sampleManifest, false); 143 | expect(newProfile).to.have.property('objectPermissions'); 144 | expect(newProfile.objectPermissions).to.deep.equal([ 145 | { 146 | object: 'Foo__c', 147 | allowRead: true, 148 | }, 149 | { 150 | object: 'Bar__c', 151 | allowRead: true, 152 | }, 153 | ]); 154 | }); 155 | it('filters fieldPermissions for Objects not in the manifest and understands Activity/Event/Task equivalence', () => { 156 | const newProfile = profileRewriter(sampleProfile as CorrectedProfile, sampleManifest, false); 157 | expect(newProfile.fieldPermissions).to.deep.equal([ 158 | { 159 | field: 'Foo__c.Foo_Field__c', 160 | readable: true, 161 | editable: true, 162 | }, 163 | { 164 | field: 'Event.Event_Field__c', 165 | readable: true, 166 | editable: true, 167 | }, 168 | { 169 | field: 'Task.Task_Field__c', 170 | readable: true, 171 | editable: true, 172 | }, 173 | ]); 174 | }); 175 | it('omits properties when there are no values after filtering', () => { 176 | const newProfile = profileRewriter( 177 | sampleProfile as CorrectedProfile, 178 | new Map([['ApexPage', ['Foo']]]), 179 | false 180 | ); 181 | expect(newProfile).to.not.have.property('objectPermissions'); 182 | expect(newProfile).to.not.have.property('fieldPermissions'); 183 | expect(newProfile).to.have.property('pageAccesses'); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /test/package/bundleCreate.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import { expect } from 'chai'; 19 | import { Connection, SfProject } from '@salesforce/core'; 20 | import { instantiateContext, restoreContext, stubContext, MockTestOrgData } from '@salesforce/core/testSetup'; 21 | import { BundleEntry } from '@salesforce/schemas'; 22 | import { createBundle, createPackageDirEntry } from '../../src/package/packageBundleCreate'; 23 | 24 | async function setupProject(setup: (project: SfProject) => void = () => {}) { 25 | const project = await SfProject.resolve(); 26 | 27 | setup(project); 28 | const projectDir = project.getPath(); 29 | project 30 | .getSfProjectJson() 31 | .getContents() 32 | .packageDirectories?.forEach((dir) => { 33 | if (dir.path) { 34 | const packagePath = path.join(projectDir, dir.path); 35 | fs.mkdirSync(packagePath, { recursive: true }); 36 | } 37 | }); 38 | 39 | return project; 40 | } 41 | 42 | describe('bundleCreate', () => { 43 | const testContext = instantiateContext(); 44 | const testOrg = new MockTestOrgData(); 45 | let connection: Connection; 46 | 47 | beforeEach(async () => { 48 | stubContext(testContext); 49 | connection = await testOrg.getConnection(); 50 | }); 51 | 52 | afterEach(() => { 53 | restoreContext(testContext); 54 | }); 55 | 56 | describe('createPackageDirEntry', () => { 57 | it('should add a new bundle entry to sfdx-project.json', async () => { 58 | testContext.inProject(true); 59 | const project = await setupProject((proj) => { 60 | proj.getSfProjectJson().set('packageDirectories', [ 61 | { 62 | path: 'force-app', 63 | default: true, 64 | }, 65 | ]); 66 | }); 67 | 68 | const bundleEntry = createPackageDirEntry(project, { 69 | BundleName: 'testBundle', 70 | Description: 'testBundle', 71 | }); 72 | project.getSfProjectJson().addPackageBundle(bundleEntry); 73 | const bundles = project.getSfProjectJson().getPackageBundles(); 74 | expect(bundles.length).to.equal(1); 75 | expect(bundleEntry).to.deep.equal(bundles[0]); 76 | }); 77 | 78 | it('add bundle entry - duplicate entry', async () => { 79 | testContext.inProject(true); 80 | const project = await setupProject((proj) => { 81 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 82 | }); 83 | const bundleEntry1: BundleEntry = { 84 | name: 'testBundle', 85 | versionName: 'testBundle', 86 | versionNumber: '1.0.0', 87 | versionDescription: 'testBundle', 88 | }; 89 | const bundleEntry2: BundleEntry = { 90 | name: 'testBundle', 91 | versionName: 'testBundle', 92 | versionNumber: '1.0.0', 93 | versionDescription: 'testBundle', 94 | }; 95 | project.getSfProjectJson().addPackageBundle(bundleEntry1); 96 | project.getSfProjectJson().addPackageBundle(bundleEntry2); 97 | const bundles = project.getSfProjectJson().getPackageBundles(); 98 | expect(bundles.length).to.equal(1); 99 | expect(bundleEntry1).to.deep.equal(bundles[0]); 100 | }); 101 | 102 | it('add bundle entry - non-duplicate entry', async () => { 103 | testContext.inProject(true); 104 | const project = await setupProject((proj) => { 105 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 106 | }); 107 | const bundleEntry1: BundleEntry = { 108 | name: 'testBundle', 109 | versionName: 'testBundle', 110 | versionNumber: '1.0.0', 111 | versionDescription: 'testBundle', 112 | }; 113 | const bundleEntry2: BundleEntry = { 114 | name: 'testBundle1', 115 | versionName: 'testBundle1', 116 | versionNumber: '1.0.0', 117 | versionDescription: 'testBundle1', 118 | }; 119 | project.getSfProjectJson().addPackageBundle(bundleEntry1); 120 | project.getSfProjectJson().addPackageBundle(bundleEntry2); 121 | const bundles = project.getSfProjectJson().getPackageBundles(); 122 | expect(bundles.length).to.equal(2); 123 | expect(bundleEntry1).to.deep.equal(bundles[0]); 124 | expect(bundleEntry2).to.deep.equal(bundles[1]); 125 | }); 126 | 127 | it('add bundle entry with createBundle', async () => { 128 | testContext.inProject(true); 129 | const project = await setupProject((proj) => { 130 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 131 | }); 132 | 133 | // Mock the connection to return a successful response 134 | Object.assign(connection.tooling, { 135 | sobject: () => ({ 136 | create: () => 137 | Promise.resolve({ 138 | success: true, 139 | id: '0Ho000000000000', 140 | }), 141 | }), 142 | }); 143 | 144 | const bundleEntry1: BundleEntry = { 145 | name: 'testBundle', 146 | versionName: 'ver 0.1', 147 | versionNumber: '0.1', 148 | versionDescription: 'testBundle', 149 | }; 150 | 151 | await createBundle(connection, project, { 152 | BundleName: 'testBundle', 153 | Description: 'testBundle', 154 | }); 155 | 156 | const bundles = project.getSfProjectJson().getPackageBundles(); 157 | expect(bundles.length).to.equal(1); 158 | expect(bundleEntry1).to.deep.equal(bundles[0]); 159 | }); 160 | 161 | it('handles failed bundle creation', async () => { 162 | testContext.inProject(true); 163 | const project = await setupProject((proj) => { 164 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 165 | }); 166 | 167 | // Mock the connection to return a failed response 168 | Object.assign(connection.tooling, { 169 | sobject: () => ({ 170 | create: () => 171 | Promise.resolve({ 172 | success: false, 173 | errors: ['Test error'], 174 | }), 175 | }), 176 | }); 177 | 178 | try { 179 | await createBundle(connection, project, { 180 | BundleName: 'testBundle', 181 | Description: 'testBundle', 182 | }); 183 | expect.fail('Expected error was not thrown'); 184 | } catch (err) { 185 | const error = err as Error; 186 | expect(error.message).to.include('Failed to create package bundle'); 187 | } 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developing 2 | 3 | ## Table of Contents 4 | 5 | [One-time Setup](#one-time-setup)
6 | [Quick Start](#quick-start)
7 | [Testing](#testing)
8 | 9 | - [Manual Testing](#manual-testing-with-repl)
10 | - [Unit Testing](#unit-tests)
11 | - [NUTs Testing](#nuts-non-unit-tests) 12 | 13 | [Debugging](#debugging)
14 | [Linking to the Packaging Plugin](#linking-to-the-packaging-plugin)
15 | [TypeScript Module Conflicts](#typescript-module-conflicts)
16 | [Useful Yarn Commands](#useful-yarn-commands)
17 | 18 |
19 | 20 | ## One-time Setup 21 | 22 | 1. Install NodeJS. If you need to work with multiple versions of Node, you 23 | might consider using [nvm](https://github.com/creationix/nvm).
_Suggestion: use the current [LTS version of node](https://github.com/nodejs/release#release-schedule)._ 24 | 1. Install [yarn](https://yarnpkg.com/) to manage node dependencies.
_Suggestion: install yarn globally using `npm install --global yarn`_ 25 | 1. Clone this repository from git. E.g., (ssh):
`git clone git@github.com:forcedotcom/packaging.git` 26 | 1. Configure [git commit signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). 27 | 28 | ## Quick Start 29 | 30 | 1. `cd` into the `packaging` directory 31 | 1. Checkout the main branch: `git checkout main` 32 | 1. Get all latest changes: `git pull` 33 | 1. Download NPM dependencies: `yarn install`. If it's been a while since you last did this you may want to run `yarn clean-all` before this step. 34 | 1. Build and lint the code: `yarn build` 35 | 1. Create a branch off main for new work: `git checkout -b ` _Suggestion: use branch_name format of initials/work-title_. For external contributors, please fork the main branch of the repo instead and PR the fork to the main branch. 36 | 1. Make code changes and build: `yarn build` 37 | 1. Write tests and run: `yarn test` (unit) and/or `yarn test:nuts` (NUTs) 38 | 1. Show all changed files: `git status` 39 | 1. Add all files to staging: `git add .` 40 | 1. Commit staged files with helpful commit message: `git commit`. New features should prepend the commit message with "feat:". Bug fixes should prepend, "fix:". 41 | 1. Push commit(s) to remote: `git push -u origin ` 42 | 1. Create a pull request (PR) using the GitHub UI [here](https://github.com/forcedotcom/packaging). 43 | 44 | ## Testing 45 | 46 | All changes must have associated tests. This library uses a combination of unit testing and NUTs (non-unit tests). You can also manually test the library using the REPL script. 47 | 48 | ### Manual Testing with REPL 49 | 50 | To manually test your changes you can simply run `yarn repl` and send input to any of the 4 main library classes; `Package`, `PackageVersion`, `SubscriberPackageVersion`, and `Package1Version`. "REPL" is an acronym for Read-Evaluate-Print-Loop, and provides a convenient way to quickly test JavaScript code. Most methods on the 4 classes require at least an org `Connection` so use the `getConnection(username)` function and pass the username or alias of an existing, CLI-authed target org. If the API you're calling also requires a DX project, get an instance of `SfProject` to use in the REPL by providing the absolute path to your project directory. 51 | 52 | The REPL script also starts a debugger process you can attach to with your preferred editor. See the [Debugging section](#debugging) for details of how to attach to the REPL debugger process. 53 | 54 | ### Unit tests 55 | 56 | Unit tests are run with `yarn test` and use the mocha test framework. Tests are located in the test directory and are named with the pattern, `.test.ts`. E.g., [package.test.ts](test/package/package.test.ts). Reference the existing unit tests when writing and testing code changes. 57 | 58 | ### NUTs (non-unit tests) 59 | 60 | Non-unit tests are run with `yarn test:nuts` and use the [cli-plugin-testkit](https://github.com/salesforcecli/cli-plugins-testkit) framework. These tests run using the default devhub in your environment and the test project located in `test/package/resources/packageProject`. This is a way to test the library code in a real environment versus a unit test environment where many things are stubbed. 61 | 62 | ## Debugging 63 | 64 | If you need to debug library code or tests you should refer to the excellent documentation on this topic in the [Plugin Developer Guide](https://github.com/salesforcecli/cli/wiki/Debug-Your-Plugin). It may be easiest to use the [REPL script](#manual-testing-with-repl) with your debugger. 65 | 66 | ## Linking to the packaging plugin 67 | 68 | When you want to use a branch of this repo in the packaging plugin to test changes, follow these steps: 69 | 70 | 1. With the library changes built (e.g., `yarn build`), link the library by running `yarn link`. 71 | 1. `cd` to `plugin-packaging` and run `yarn clean-all`. 72 | 1. Download NPM dependencies: `yarn install`. 73 | 1. Use the linked packaging library: `yarn link "@salesforce/packaging"`. 74 | 1. Build and lint the code: `yarn build`. If you get TypeScript module conflict errors during this step, see section below on TypeScript module conflicts. 75 | 76 | ## TypeScript Module Conflicts 77 | 78 | During TypeScript compilation, you may see errors such as: 79 | 80 | `error TS2322: Type 'import(".../plugin-packaging/node_modules/@salesforce/core/lib/org/connection").Connection' is not assignable to type 'import(".../packaging/node_modules/@salesforce/core/lib/org/connection").Connection'.` 81 | 82 | This means the `Connection` interface in the core library used by the **packaging plugin** is different from the `Connection` interface in the core library used by the **packaging library**, most likely because the core library dependencies are different versions. 83 | 84 | To fix this we need to tell the TypeScript compiler to use 1 version of that library. To do this, temporarily modify the [tsconfig.json](tsconfig.json) file with the following lines inside the `compilerOptions` section and recompile: 85 | 86 | ```json 87 | "baseUrl": ".", 88 | "paths": { 89 | "@salesforce/core": ["node_modules/@salesforce/core"] 90 | } 91 | ``` 92 | 93 | If there are conflict errors in the tests then we need to make a similar modification to the [test/tsconfig.json](test/tsconfig.json) file. Note that the `baseUrl` property for this modification points to the directory above: 94 | 95 | ```json 96 | "baseUrl": "..", 97 | "paths": { 98 | "@salesforce/core": ["node_modules/@salesforce/core"] 99 | } 100 | ``` 101 | 102 | **_Note that these are temporary changes for linked compilation and should not be committed._** 103 | 104 | ## Useful yarn commands 105 | 106 | #### `yarn install` 107 | 108 | This downloads all NPM dependencies into the node_modules directory. 109 | 110 | #### `yarn compile` 111 | 112 | This compiles the typescript to javascript. 113 | 114 | #### `yarn lint` 115 | 116 | This lints all the typescript using eslint. 117 | 118 | #### `yarn build` 119 | 120 | This compiles and lints all the typescript (e.g., `yarn compile && yarn lint`). 121 | 122 | #### `yarn clean` 123 | 124 | This cleans all generated files and directories. Run `yarn clean-all` to also clean up the node_module directories. 125 | 126 | #### `yarn test` 127 | 128 | This runs unit tests (mocha) for the project using ts-node. 129 | 130 | #### `yarn test:nuts` 131 | 132 | This runs NUTs (non-unit tests) for the project using ts-node. 133 | -------------------------------------------------------------------------------- /src/package/packageProfileApi.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import os from 'node:os'; 18 | import fs from 'node:fs'; 19 | import globby from 'globby'; 20 | import { Logger, Messages, SfProject } from '@salesforce/core'; 21 | import { AsyncCreatable } from '@salesforce/kit'; 22 | import { PackageXml, ProfileApiOptions } from '../interfaces'; 23 | import { 24 | CorrectedProfile, 25 | manifestTypesToMap, 26 | profileObjectToString, 27 | profileRewriter, 28 | profileStringToProfile, 29 | } from './profileRewriter'; 30 | 31 | Messages.importMessagesDirectory(__dirname); 32 | const profileApiMessages = Messages.loadMessages('@salesforce/packaging', 'profile_api'); 33 | 34 | /* 35 | * This class provides functions used to re-write .profiles in the project package directories when creating a package2 version. 36 | * All profiles found in the project package directories are extracted out and then re-written to only include metadata in the 37 | * profile that is relevant to the source in the package directory being packaged. 38 | */ 39 | export class PackageProfileApi extends AsyncCreatable { 40 | public project: SfProject; 41 | public includeUserLicenses = false; 42 | 43 | public constructor(options: ProfileApiOptions) { 44 | super(options); 45 | this.project = options.project; 46 | this.includeUserLicenses = options.includeUserLicenses ?? false; 47 | } 48 | 49 | // eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-empty-function 50 | public async init(): Promise {} 51 | 52 | /** 53 | * For any profile present in the project package directories, this function generates a subset of data that only 54 | * contains references to items in the manifest. 55 | * 56 | * return a list of profile file locations that need to be removed from the package because they are empty 57 | * 58 | * @param destPath location of new profiles 59 | * @param manifestTypes: array of objects { name: string, members: string[] } that represent package xml types 60 | * @param excludedDirectories Directories to not include profiles from 61 | */ 62 | public generateProfiles( 63 | destPath: string, 64 | manifestTypes: PackageXml['types'], 65 | excludedDirectories: string[] = [] 66 | ): string[] { 67 | const logger = Logger.childFromRoot('PackageProfileApi'); 68 | 69 | return ( 70 | this.getProfilesWithNamesAndPaths(excludedDirectories) 71 | .map(({ profilePath, name: profileName }) => { 72 | const originalProfile = profileStringToProfile(fs.readFileSync(profilePath, 'utf-8')); 73 | const adjustedProfile = profileRewriter( 74 | originalProfile, 75 | manifestTypesToMap(manifestTypes), 76 | this.includeUserLicenses 77 | ); 78 | return { 79 | profileName, 80 | profilePath, 81 | hasContent: Object.keys(adjustedProfile).length, 82 | adjustedProfile, 83 | removedSettings: getRemovedSettings(originalProfile, adjustedProfile), 84 | xmlFileLocation: getXmlFileLocation(destPath, profilePath), 85 | }; 86 | }) 87 | // side effect: modify profiles in place 88 | .filter(({ hasContent, profileName, removedSettings, profilePath, xmlFileLocation, adjustedProfile }) => { 89 | if (!hasContent) { 90 | logger.warn( 91 | `Profile ${profileName} has no content after filtering. It will still be part of the package but you can remove it if it's not needed.` 92 | ); 93 | return true; 94 | } else { 95 | logger.info(profileApiMessages.getMessage('addProfileToPackage', [profileName, profilePath])); 96 | removedSettings.forEach((setting) => { 97 | logger.info(profileApiMessages.getMessage('removeProfileSetting', [setting, profileName])); 98 | }); 99 | fs.writeFileSync(xmlFileLocation, profileObjectToString(adjustedProfile), 'utf-8'); 100 | } 101 | }) 102 | .map(({ xmlFileLocation }) => xmlFileLocation.replace(/(.*)(\.profile)/, '$1')) 103 | ); 104 | } 105 | 106 | /** 107 | * Filter out all profiles in the manifest and if any profiles exist in the project package directories, add them to the manifest. 108 | * 109 | * @param typesArr array of objects { name[], members[] } that represent package types JSON. 110 | * @param excludedDirectories Direcotires not to generate profiles for 111 | */ 112 | public filterAndGenerateProfilesForManifest( 113 | typesArr: PackageXml['types'], 114 | excludedDirectories: string[] = [] 115 | ): PackageXml['types'] { 116 | const profilePathsWithNames = this.getProfilesWithNamesAndPaths(excludedDirectories); 117 | 118 | // Filter all profiles, and add back the ones we found names for 119 | return typesArr 120 | .filter((kvp) => kvp.name !== 'Profile') 121 | .concat([{ name: 'Profile', members: profilePathsWithNames.map((i) => i.name) }]); 122 | } 123 | 124 | // Look for profiles in all package directories 125 | private findAllProfiles(excludedDirectories: string[] = []): string[] { 126 | const ignore = excludedDirectories.map((dir) => `**/${dir.split(path.sep).join(path.posix.sep)}/**`); 127 | const patterns = this.project 128 | .getUniquePackageDirectories() 129 | .map((pDir) => pDir.fullPath) 130 | .map((fullDir) => 131 | os.type() === 'Windows_NT' 132 | ? path.posix.join(...fullDir.split(path.sep), '**', '*.profile-meta.xml') 133 | : path.join(fullDir, '**', '*.profile-meta.xml') 134 | ); 135 | return globby.sync(patterns, { ignore }); 136 | } 137 | 138 | private getProfilesWithNamesAndPaths(excludedDirectories: string[]): Array> { 139 | return this.findAllProfiles(excludedDirectories) 140 | .map((profilePath) => ({ profilePath, name: profilePathToName(profilePath) })) 141 | .filter(isProfilePathWithName); 142 | } 143 | } 144 | 145 | type ProfilePathWithName = { profilePath: string; name?: string }; 146 | 147 | const isProfilePathWithName = ( 148 | profilePathWithName: ProfilePathWithName 149 | ): profilePathWithName is Required => typeof profilePathWithName.name === 'string'; 150 | 151 | const profilePathToName = (profilePath: string): string | undefined => 152 | profilePath.match(/([^/]+)\.profile-meta.xml/)?.[1]; 153 | 154 | const getXmlFileLocation = (destPath: string, profilePath: string): string => 155 | path.join(destPath, path.basename(profilePath).replace(/(.*)(-meta.xml)/, '$1')); 156 | 157 | const getRemovedSettings = (originalProfile: CorrectedProfile, adjustedProfile: CorrectedProfile): string[] => { 158 | const originalProfileSettings = Object.keys(originalProfile); 159 | const adjustedProfileSettings = new Set(Object.keys(adjustedProfile)); 160 | return originalProfileSettings.filter((setting) => !adjustedProfileSettings.has(setting)); 161 | }; 162 | -------------------------------------------------------------------------------- /messages/package_version_create.md: -------------------------------------------------------------------------------- 1 | # errorPackageAndPackageIdCollision 2 | 3 | You can’t have both "package" and "packageId" (deprecated) defined as dependencies in sfdx-project.json. 4 | 5 | # errorPackageOrPackageIdMissing 6 | 7 | You must provide either "package" or "packageId" (deprecated) defined as dependencies in sfdx-project.json. 8 | 9 | # errorDependencyPair 10 | 11 | Dependency must specify either a subscriberPackageVersionId or both packageId and versionNumber: %s 12 | 13 | # errorNoIdInHub 14 | 15 | No package ID was found in Dev Hub for package ID: %s. 16 | 17 | # versionNumberNotFoundInDevHub 18 | 19 | No version number was found in Dev Hub for package id %s and branch %s and version number %s that resolved to build number %s. 20 | 21 | # buildNumberResolvedForLatest 22 | 23 | Dependency on package %s was resolved to version number %s, branch %s, %s. 24 | 25 | # buildNumberResolvedForReleased 26 | 27 | Dependency on package %s was resolved to the released version number %s, %s. 28 | 29 | # noReleaseVersionFound 30 | 31 | No released version was found in Dev Hub for package id %s and version number %s. 32 | 33 | # noReleaseVersionFoundForBranch 34 | 35 | No version number was found in Dev Hub for package id %s and branch %s and version number %s. 36 | 37 | # tempFileLocation 38 | 39 | The temp files are located at: %s. 40 | 41 | # signupDuplicateSettingsSpecified 42 | 43 | You cannot use 'settings' and 'orgPreferences' in your scratch definition file, please specify one or the other. 44 | 45 | # errorReadingDefintionFile 46 | 47 | There was an error while reading or parsing the provided scratch definition file: %s 48 | 49 | # invalidPatchVersionSpecified 50 | 51 | Requested patch version number %s to convert is invalid 52 | 53 | # seedMDDirectoryDoesNotExist 54 | 55 | Seed metadata directory %s was specified but does not exist. 56 | 57 | # unpackagedMDDirectoryDoesNotExist 58 | 59 | Un-packaged metadata directory %s was specified but does not exist. 60 | 61 | # errorEmptyPackageDirs 62 | 63 | sfdx-project.json must contain a packageDirectories entry for a package. You can run the force:package:create command to auto-populate such an entry. 64 | 65 | # failedToCreatePVCRequest 66 | 67 | Failed to create request %s: %s 68 | 69 | # errorScriptsNotApplicableToUnlockedPackage 70 | 71 | We can’t create the package version. This parameter is available only for second-generation managed packages. Create the package version without the postinstallscript or uninstallscript parameters. 72 | 73 | # errorAncestorNotApplicableToUnlockedPackage 74 | 75 | Can’t create package version. Specifying an ancestor is available only for second-generation managed packages. Remove the ancestorId or ancestorVersion from your sfdx-project.json file, and then create the package version again. 76 | 77 | # defaultVersionName 78 | 79 | versionName is blank in sfdx-project.json, so it will be set to this default value based on the versionNumber: %s 80 | 81 | # malformedUrl 82 | 83 | The %s value "%s" from the command line or sfdx-project.json is not in the correct format for a URL. It must be a valid URL in the format "http://salesforce.com". More information: https://nodejs.org/api/url.html#url_url_strings_and_url_objects 84 | 85 | # versionCreateFailedWithMultipleErrors 86 | 87 | Multiple errors occurred: 88 | 89 | # invalidDaysNumber 90 | 91 | Provide a valid positive number for %s. %d 92 | 93 | # errorAncestorNoneNotAllowed 94 | 95 | Can’t create package version because you didn’t specify a package ancestor. Set the ancestor version to %s and try creating the package version again. You can also specify --skip-ancestor-check to override the ancestry requirement. 96 | 97 | # errorAncestorNotHighest 98 | 99 | Can’t create package version. The ancestor version [%s] you specified isn’t the highest released package version. Set the ancestor version to %s and try creating the package version again. You can also specify --skip-ancestor-check to override the ancestry requirement. 100 | 101 | # errorInvalidBuildNumberForKeywords 102 | 103 | The provided VersionNumber '%s' is invalid. Provide an integer value or use the keyword '%s' or '%s' for the build number. 104 | 105 | # errorInvalidBuildNumber 106 | 107 | The provided VersionNumber '%s' is invalid. Provide an integer value or use the keyword '%s' for the build number. 108 | 109 | # errorNoSubscriberPackageRecord 110 | 111 | No subscriber package was found for seed id: %s 112 | 113 | # errorMoreThanOnePackage2WithSeed 114 | 115 | Only one package in a Dev Hub is allowed per converted from first-generation package, but the following were found: 116 | %s 117 | 118 | # errorMissingPackageIdOrPath 119 | 120 | You must specify either a package ID or a package path to create a new package version. 121 | 122 | # errorMissingPackage 123 | 124 | The package "%s" isn’t defined in the sfdx-project.json file. Add it to the packageDirectories section and add the alias to packageAliases with its 0Ho ID. 125 | 126 | # errorCouldNotFindPackageUsingPath 127 | 128 | Could not find a package in sfdx-project.json file using "path" %s. Add it to the packageDirectories section and add the alias to packageAliases with its 0Ho ID. 129 | 130 | # errorCouldNotFindPackageDir 131 | 132 | Couldn't find a package directory for package using %s %s. Add it to the packageDirectories section and add the alias to packageAliases with its 0Ho ID. 133 | 134 | # noSourceInRootDirectory 135 | 136 | No matching source was found within the package root directory: %s 137 | 138 | # packageXmlDoesNotContainPackage 139 | 140 | While preparing package version create request, the calculated package.xml for the package does not contain a element. 141 | 142 | # packageXmlDoesNotContainPackageTypes 143 | 144 | While preparing package version create request, the calculated package.xml for the package does not contain a element. 145 | 146 | # errorInvalidPatchNumber 147 | 148 | Patch version node for version, %s, must be 0 for a Locked package. 149 | 150 | # errorAncestorIdVersionHighestOrNoneMismatch 151 | 152 | Both ancestorId (%s) and ancestorVersion (%s) specified, HIGHEST and/or NONE are used, the values disagree 153 | 154 | # errorInvalidAncestorVersionFormat 155 | 156 | The given ancestorVersion (%s) is not in the correct format 157 | 158 | # errorNoMatchingAncestor 159 | 160 | No matching ancestor found for the given ancestorVersion (%s) in package %s 161 | 162 | # errorAncestorNotReleased 163 | 164 | The given ancestor version (%s) has not been released 165 | 166 | # errorAncestorIdVersionMismatch 167 | 168 | No matching ancestor version (%s) found for the given ancestorId (%s) 169 | 170 | # errorNoMatchingMajorMinorForPatch 171 | 172 | No matching major.minor version found for the given patch version (%s) 173 | 174 | # errorInvalidPackageId 175 | 176 | The provided package ID '%s' is invalid. 177 | 178 | # packageIdCannotBeUndefined 179 | 180 | The package ID must be defined. 181 | 182 | # deploydirCannotBeUndefined 183 | 184 | The deploy directory must be defined. Supplied options: %s 185 | 186 | # packagePathCannotBeUndefined 187 | 188 | The package path must be defined. 189 | 190 | # errorMissingPackagePath 191 | 192 | The package path is missing. Supplied options: %s 193 | 194 | # versionNumberRequired 195 | 196 | The version number is required and was not found in the options or in package json descriptor. 197 | 198 | # missingConnection 199 | 200 | A connection is required. 201 | 202 | # IdUnavailableWhenQueued 203 | 204 | Request is queued. ID unavailable. 205 | 206 | # IdUnavailableWhenInProgress 207 | 208 | Request is in progress. ID unavailable. 209 | 210 | # IdUnavailableWhenError 211 | 212 | ID Unavailable 213 | -------------------------------------------------------------------------------- /test/package/packageCreate.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import path from 'node:path'; 17 | import fs from 'node:fs'; 18 | import { expect } from 'chai'; 19 | import { instantiateContext, restoreContext, stubContext } from '@salesforce/core/testSetup'; 20 | import { SfProject } from '@salesforce/core'; 21 | import { createPackageRequestFromContext, createPackageDirEntry } from '../../src/package/packageCreate'; 22 | 23 | async function setupProject(setup: (project: SfProject) => void = () => {}) { 24 | const project = await SfProject.resolve(); 25 | const packageDirectories = [ 26 | { 27 | path: 'force-app', 28 | default: true, 29 | }, 30 | ]; 31 | const packageAliases = {}; 32 | project.getSfProjectJson().set('packageDirectories', packageDirectories); 33 | project.getSfProjectJson().set('packageAliases', packageAliases); 34 | setup(project); 35 | const projectDir = project.getPath(); 36 | project 37 | .getSfProjectJson() 38 | .getContents() 39 | .packageDirectories?.forEach((dir) => { 40 | if (dir.path) { 41 | const packagePath = path.join(projectDir, dir.path); 42 | fs.mkdirSync(packagePath, { recursive: true }); 43 | } 44 | }); 45 | 46 | return project; 47 | } 48 | 49 | describe('packageCreate', () => { 50 | const $$ = instantiateContext(); 51 | 52 | beforeEach(() => { 53 | stubContext($$); 54 | }); 55 | 56 | afterEach(() => { 57 | restoreContext($$); 58 | }); 59 | 60 | describe('_createPackage2RequestFromContext', () => { 61 | it('should return a valid request', async () => { 62 | $$.inProject(true); 63 | const project = await setupProject(); 64 | const request = createPackageRequestFromContext(project, { 65 | name: 'test', 66 | description: 'test description', 67 | path: 'test/path', 68 | packageType: 'Managed', 69 | orgDependent: false, 70 | errorNotificationUsername: 'foo@bar.org', 71 | noNamespace: false, 72 | }); 73 | expect(request).to.deep.equal({ 74 | ContainerOptions: 'Managed', 75 | Description: 'test description', 76 | IsOrgDependent: false, 77 | Name: 'test', 78 | NamespacePrefix: '', 79 | PackageErrorUsername: 'foo@bar.org', 80 | }); 81 | }); 82 | it('should return a valid request for a namespace', async () => { 83 | $$.inProject(true); 84 | const project = await setupProject((proj) => { 85 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 86 | }); 87 | const request = createPackageRequestFromContext(project, { 88 | name: 'test', 89 | description: 'test description', 90 | path: 'test/path', 91 | packageType: 'Managed', 92 | orgDependent: false, 93 | errorNotificationUsername: 'foo@bar.org', 94 | noNamespace: false, 95 | }); 96 | expect(request).to.deep.equal({ 97 | ContainerOptions: 'Managed', 98 | Description: 'test description', 99 | IsOrgDependent: false, 100 | Name: 'test', 101 | NamespacePrefix: 'testNamespace', 102 | PackageErrorUsername: 'foo@bar.org', 103 | }); 104 | }); 105 | it('should return a valid no namespace request for a namespaced package', async () => { 106 | $$.inProject(true); 107 | const project = await setupProject((proj) => { 108 | proj.getSfProjectJson().set('namespace', 'testNamespace'); 109 | }); 110 | const request = createPackageRequestFromContext(project, { 111 | name: 'test', 112 | description: 'test description', 113 | path: 'test/path', 114 | packageType: 'Managed', 115 | orgDependent: false, 116 | errorNotificationUsername: 'foo@bar.org', 117 | noNamespace: true, 118 | }); 119 | expect(request).to.deep.equal({ 120 | ContainerOptions: 'Managed', 121 | Description: 'test description', 122 | IsOrgDependent: false, 123 | Name: 'test', 124 | NamespacePrefix: '', 125 | PackageErrorUsername: 'foo@bar.org', 126 | }); 127 | }); 128 | describe('createPackageDirEntry', () => { 129 | it('should return a valid new package directory entry - no existing entries', async () => { 130 | $$.inProject(true); 131 | const project = await setupProject((proj) => { 132 | proj.getSfProjectJson().set('packageDirectories', []); 133 | }); 134 | 135 | const packageDirEntry = createPackageDirEntry(project, { 136 | name: 'test', 137 | description: 'test description', 138 | path: 'test/path', 139 | packageType: 'Managed', 140 | orgDependent: false, 141 | errorNotificationUsername: 'foo@bar.org', 142 | noNamespace: true, 143 | }); 144 | expect(packageDirEntry).to.deep.equal({ 145 | default: true, 146 | package: 'test', 147 | path: 'test/path', 148 | versionDescription: 'test description', 149 | versionName: 'ver 0.1', 150 | versionNumber: '0.1.0.NEXT', 151 | }); 152 | }); 153 | it('should return a valid new package directory entry - existing entries', async () => { 154 | $$.inProject(true); 155 | const project = await setupProject(); 156 | 157 | const packageDirEntry = createPackageDirEntry(project, { 158 | name: 'test', 159 | description: 'test description', 160 | path: 'test/path', 161 | packageType: 'Managed', 162 | orgDependent: false, 163 | errorNotificationUsername: 'foo@bar.org', 164 | noNamespace: true, 165 | }); 166 | expect(packageDirEntry).to.deep.equal({ 167 | default: false, 168 | package: 'test', 169 | path: 'test/path', 170 | versionDescription: 'test description', 171 | versionName: 'ver 0.1', 172 | versionNumber: '0.1.0.NEXT', 173 | }); 174 | expect(packageDirEntry).to.not.deep.equal(project.getSfProjectJson().getContents().packageDirectories[0]); 175 | }); 176 | it('should return a valid modified package directory entry', async () => { 177 | $$.inProject(true); 178 | const project = await setupProject((proj) => { 179 | const packageDirectories = proj.getSfProjectJson().getContents().packageDirectories; 180 | packageDirectories.push({ 181 | path: 'test/path', 182 | versionName: 'ver 0.1', 183 | versionNumber: '0.1.0.NEXT', 184 | }); 185 | proj.getSfProjectJson().set('packageDirectories', packageDirectories); 186 | }); 187 | const packageDirEntry = createPackageDirEntry(project, { 188 | name: 'test-01', 189 | description: 'test description', 190 | path: 'test/path', 191 | packageType: 'Managed', 192 | orgDependent: false, 193 | errorNotificationUsername: 'foo@bar.org', 194 | noNamespace: true, 195 | }); 196 | expect(packageDirEntry).to.to.have.property('package', 'test-01'); 197 | expect(packageDirEntry.package).to.not.be.true; 198 | }); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /src/package1/package1Version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import os from 'node:os'; 17 | import type { Schema } from '@jsforce/jsforce-node'; 18 | import { Connection, Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core'; 19 | import { Duration } from '@salesforce/kit'; 20 | import { 21 | IPackageVersion1GP, 22 | Package1VersionCreateRequest, 23 | Package1VersionEvents, 24 | PackagingSObjects, 25 | } from '../interfaces'; 26 | import MetadataPackageVersion = PackagingSObjects.MetadataPackageVersion; 27 | 28 | Messages.importMessagesDirectory(__dirname); 29 | const messages = Messages.loadMessages('@salesforce/packaging', 'package1Version'); 30 | 31 | /** 32 | * Provides the ability to get, list, and create 1st generation package versions. 33 | * 34 | * **Examples** 35 | * 36 | * List all 1GP package versions in the org: 37 | * 38 | * `const pkgList = await Package1Version.list(connection);` 39 | * 40 | * Create a new 1GP package version in the org: 41 | * 42 | * `const myPkg = await Package1Version.create(connection, options, pollingOptions);` 43 | * 44 | * More implementation examples are in the plugin here: https://github.com/salesforcecli/plugin-packaging/tree/main/src/commands/force/package1/ 45 | */ 46 | export class Package1Version implements IPackageVersion1GP { 47 | /** 48 | * Package1Version Constructor - Class to be used with 1st generation package versions 49 | * 50 | * @param connection: Connection to the org 51 | * @param id: 04t ID of the package version 52 | */ 53 | public constructor(private connection: Connection, private id: string) { 54 | if (!id.startsWith('04t')) { 55 | throw messages.createError('invalid04tId', [id]); 56 | } 57 | } 58 | 59 | /** 60 | * Will create a PackageUploadRequest object based on the options provided, will poll for completion if pollingOptions are provided 61 | * 62 | * @param connection: Connection to the org 63 | * @param options: Package1VersionCreateRequest options for the new PackageUploadRequest to be created with 64 | * @param pollingOptions: options to set frequency, and duration of polling. Default to not poll 65 | */ 66 | public static async create( 67 | connection: Connection, 68 | options: Package1VersionCreateRequest, 69 | pollingOptions = { frequency: Duration.seconds(5), timeout: Duration.seconds(0) } 70 | ): Promise { 71 | if (!options.MetadataPackageId?.startsWith('033')) { 72 | throw messages.createError('missingMetadataPackageId'); 73 | } 74 | if (!options.VersionName) { 75 | throw messages.createError('missingVersionName'); 76 | } 77 | const createRequest = await connection.tooling.sobject('PackageUploadRequest').create(options); 78 | if (createRequest.success) { 79 | if (pollingOptions.timeout.seconds) { 80 | const timeout = pollingOptions.timeout.seconds; 81 | const pollingClient = await PollingClient.create({ 82 | poll: () => packageUploadPolling(connection, createRequest.id, timeout, pollingOptions.frequency.seconds), 83 | ...pollingOptions, 84 | }); 85 | return pollingClient.subscribe(); 86 | } else { 87 | // jsforce templates weren't working when setting the type to PackageUploadRequest, so we have to cast `as unknown as PackagingSObjects.PackageUploadRequest` 88 | return (await connection.tooling 89 | .sobject('PackageUploadRequest') 90 | .retrieve(createRequest.id)) as unknown as PackagingSObjects.PackageUploadRequest; 91 | } 92 | } else { 93 | throw messages.createError('createFailed', [JSON.stringify(createRequest)]); 94 | } 95 | } 96 | 97 | /** 98 | * Returns the status of a PackageUploadRequest 99 | * 100 | * @param connection Connection to the target org 101 | * @param id 0HD Id of the PackageUploadRequest 102 | */ 103 | public static async getCreateStatus( 104 | connection: Connection, 105 | id: string 106 | ): Promise { 107 | if (!id.startsWith('0HD')) { 108 | throw messages.createError('invalid0HDId', [id]); 109 | } 110 | return (await connection.tooling 111 | .sobject('PackageUploadRequest') 112 | .retrieve(id)) as unknown as PackagingSObjects.PackageUploadRequest; 113 | } 114 | 115 | /** 116 | * Lists package versions available in the org. If package ID is supplied, only list versions of that package, 117 | * otherwise, list all package versions, up to 10,000. If more records are needed use the `SF_ORG_MAX_QUERY_LIMIT` env var. 118 | * 119 | * @param connection Connection to the org 120 | * @param id: optional, if present, ID of package to list versions for (starts with 033) 121 | * @returns Array of package version results 122 | */ 123 | public static async list(connection: Connection, id?: string): Promise { 124 | if (id && !id?.startsWith('033')) { 125 | // we have to check that it is present, and starts with 033 126 | // otherwise, undefined doesn't start with 033 and will trigger this error, when it shouldn't 127 | throw messages.createError('invalid033Id', [id]); 128 | } 129 | const query = `SELECT Id,MetadataPackageId,Name,ReleaseState,MajorVersion,MinorVersion,PatchVersion,BuildNumber FROM MetadataPackageVersion ${ 130 | id ? `WHERE MetadataPackageId = '${id}'` : '' 131 | } ORDER BY MetadataPackageId, MajorVersion, MinorVersion, PatchVersion, BuildNumber`; 132 | 133 | return ( 134 | await connection.autoFetchQuery(query, { 135 | tooling: true, 136 | }) 137 | )?.records; 138 | } 139 | 140 | /** 141 | * Queries the org for the package version with the given ID 142 | */ 143 | public async getPackageVersion(): Promise { 144 | const query = `SELECT Id, MetadataPackageId, Name, ReleaseState, MajorVersion, MinorVersion, PatchVersion, BuildNumber FROM MetadataPackageVersion WHERE id = '${this.id}'`; 145 | return (await this.connection.tooling.query(query)).records; 146 | } 147 | } 148 | 149 | const packageUploadPolling = async ( 150 | connection: Connection, 151 | id: string, 152 | timeout: number, 153 | frequency: number 154 | ): Promise => { 155 | const pollingResult = await connection.tooling.sobject('PackageUploadRequest').retrieve(id); 156 | switch (pollingResult.Status) { 157 | case 'SUCCESS': 158 | return { completed: true, payload: pollingResult }; 159 | case 'IN_PROGRESS': 160 | case 'QUEUED': 161 | timeout -= frequency; 162 | await Lifecycle.getInstance().emit(Package1VersionEvents.create.progress, { timeout, pollingResult }); 163 | return { completed: false, payload: pollingResult }; 164 | default: { 165 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 166 | const errors = pollingResult?.Errors?.errors as Error[]; 167 | if (errors?.length > 0) { 168 | throw messages.createError('package1VersionCreateCommandUploadFailure', [ 169 | errors.map((e: Error) => e.message).join(os.EOL), 170 | ]); 171 | } else { 172 | throw messages.createError('package1VersionCreateCommandUploadFailureDefault'); 173 | } 174 | } 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /test/package/packageVersionList.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { expect } from 'chai'; 17 | import { SfProject } from '@salesforce/core'; 18 | import { instantiateContext, restoreContext, stubContext } from '@salesforce/core/testSetup'; 19 | import { 20 | assembleQueryParts, 21 | constructWhere, 22 | DEFAULT_ORDER_BY_FIELDS, 23 | validateDays, 24 | constructQuery, 25 | } from '../../src/package/packageVersionList'; 26 | 27 | describe('package version list', () => { 28 | const $$ = instantiateContext(); 29 | 30 | beforeEach(() => { 31 | stubContext($$); 32 | }); 33 | 34 | afterEach(() => { 35 | restoreContext($$); 36 | }); 37 | 38 | describe('_getLastDays', () => { 39 | it('should return the last days of 7', () => { 40 | expect(validateDays('seven', 7)).to.equal(7); 41 | }); 42 | it('should return the last days of 0', () => { 43 | expect(validateDays('zero', 0)).to.equal(0); 44 | }); 45 | it('should throw with negative number as input', () => { 46 | expect(() => validateDays('negative', -1)).to.throw(/Provide a valid positive number for negative. -1/); 47 | }); 48 | it('should throw missing lastDays input', () => { 49 | expect(() => validateDays('negative')).to.throw(/Provide a valid positive number for negative. -1/); 50 | }); 51 | it('should throw with undefined as input', () => { 52 | expect(() => validateDays('negative', undefined)).to.throw(/Provide a valid positive number for negative. -1/); 53 | }); 54 | }); 55 | describe('_constructWhere', () => { 56 | // the following package dirs and aliases were extracted from the Salesforce Dreamhouse LWC repo 57 | const packageDirectories = [ 58 | { 59 | path: 'force-app', 60 | default: true, 61 | package: 'DreamhouseLWC', 62 | versionName: "Summer '21", 63 | versionNumber: '53.0.0.NEXT', 64 | }, 65 | ]; 66 | 67 | const packageAliases = { 68 | DreamhouseLWC: '0Ho3h000000xxxxCAG', 69 | }; 70 | 71 | it('should create where clause contain proper values', async () => { 72 | $$.inProject(true); 73 | const sfProject = await SfProject.resolve(); 74 | sfProject.getSfProjectJson().set('packageDirectories', packageDirectories); 75 | sfProject.getSfProjectJson().set('packageAliases', packageAliases); 76 | const where = constructWhere({ 77 | packages: ['0Ho3h000000xxxxCAG'], 78 | createdLastDays: 1, 79 | modifiedLastDays: 2, 80 | isReleased: true, 81 | }); 82 | expect(where).to.include("Package2Id IN ('0Ho3h000000xxxxCAG')"); 83 | expect(where).to.include('IsDeprecated = false'); 84 | expect(where).to.include('CreatedDate = LAST_N_DAYS:1'); 85 | expect(where).to.include('LastModifiedDate = LAST_N_DAYS:2'); 86 | }); 87 | 88 | it('should create where clause contain proper values and branch', async () => { 89 | $$.inProject(true); 90 | const sfProject = await SfProject.resolve(); 91 | sfProject.getSfProjectJson().set('packageDirectories', packageDirectories); 92 | sfProject.getSfProjectJson().set('packageAliases', packageAliases); 93 | const where = constructWhere({ 94 | packages: ['0Ho3h000000xxxxCAG'], 95 | createdLastDays: 1, 96 | modifiedLastDays: 2, 97 | isReleased: true, 98 | branch: 'main', 99 | }); 100 | expect(where).to.include("Package2Id IN ('0Ho3h000000xxxxCAG')"); 101 | expect(where).to.include('IsDeprecated = false'); 102 | expect(where).to.include('CreatedDate = LAST_N_DAYS:1'); 103 | expect(where).to.include('LastModifiedDate = LAST_N_DAYS:2'); 104 | expect(where).to.include("Branch='main'"); 105 | }); 106 | }); 107 | 108 | describe('_constructQuery', () => { 109 | it('should include verbose fields', async () => { 110 | const options = { 111 | packages: ['0Ho3h000000xxxxCAG'], 112 | createdLastDays: 1, 113 | modifiedLastDays: 2, 114 | isReleased: true, 115 | verbose: true, 116 | }; 117 | const constQuery = constructQuery(50, options); 118 | expect(constQuery).to.include('CodeCoverage'); 119 | expect(constQuery).to.include('HasPassedCodeCoverageCheck'); 120 | expect(constQuery).to.not.include('Language'); 121 | }); 122 | 123 | it('should include verbose fields with langage', async () => { 124 | const options = { 125 | packages: ['0Ho3h000000xxxxCAG'], 126 | createdLastDays: 1, 127 | modifiedLastDays: 2, 128 | isReleased: true, 129 | verbose: true, 130 | }; 131 | const constQuery = constructQuery(59, options); 132 | expect(constQuery).to.include('CodeCoverage'); 133 | expect(constQuery).to.include('HasPassedCodeCoverageCheck'); 134 | expect(constQuery).to.include('Language'); 135 | }); 136 | 137 | it('should not include verbose fields', async () => { 138 | const options = { 139 | packages: ['0Ho3h000000xxxxCAG'], 140 | createdLastDays: 1, 141 | modifiedLastDays: 2, 142 | isReleased: true, 143 | }; 144 | const constQuery = constructQuery(59, options); 145 | expect(constQuery).to.not.include('CodeCoverage'); 146 | expect(constQuery).to.not.include('HasPassedCodeCoverageCheck'); 147 | expect(constQuery).to.not.include('Language'); 148 | }); 149 | 150 | it('should include validatedAsync field', async () => { 151 | const options = { 152 | packages: ['0Ho3h000000xxxxCAG'], 153 | createdLastDays: 1, 154 | modifiedLastDays: 2, 155 | isReleased: true, 156 | }; 157 | const constQuery = constructQuery(61, options); 158 | expect(constQuery).to.include('ValidatedAsync'); 159 | }); 160 | 161 | it('should not include validatedAsync field', async () => { 162 | const options = { 163 | packages: ['0Ho3h000000xxxxCAG'], 164 | createdLastDays: 1, 165 | modifiedLastDays: 2, 166 | isReleased: true, 167 | }; 168 | const constQuery = constructQuery(59, options); 169 | expect(constQuery).to.not.include('ValidatedAsync'); 170 | }); 171 | }); 172 | 173 | describe('_assembleQueryParts', () => { 174 | it('should return the proper query', () => { 175 | const assembly = assembleQueryParts('select foo,bar,baz from foobarbaz', ['foo=1', "bar='2'"], 'foo,bar,baz'); 176 | expect(assembly).to.include('select foo,bar,baz from foobarbaz'); 177 | expect(assembly).to.include("WHERE foo=1 AND bar='2'"); 178 | expect(assembly).to.include('ORDER BY foo,bar,baz'); 179 | }); 180 | it('should return the proper query when no where parts supplied', () => { 181 | const assembly = assembleQueryParts('select foo,bar,baz from foobarbaz', [], 'foo,bar,baz'); 182 | expect(assembly).to.include('select foo,bar,baz from foobarbaz'); 183 | expect(assembly).not.include("WHERE foo=1 AND bar='2'"); 184 | expect(assembly).to.include('ORDER BY foo,bar,baz'); 185 | }); 186 | it('should return the proper query when no order by parts supplied', () => { 187 | const assembly = assembleQueryParts('select foo,bar,baz from foobarbaz', []); 188 | expect(assembly).to.include('select foo,bar,baz from foobarbaz'); 189 | expect(assembly).not.include("WHERE foo=1 AND bar='2'"); 190 | expect(assembly).to.include(`ORDER BY ${DEFAULT_ORDER_BY_FIELDS}`); 191 | }); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /test/package/ancestry.nut.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import os from 'node:os'; 17 | import path from 'node:path'; 18 | import { expect, config } from 'chai'; 19 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 20 | import { SfProject } from '@salesforce/core/project'; 21 | import { Org } from '@salesforce/core'; 22 | import { 23 | AncestryRepresentationProducer, 24 | AncestryRepresentationProducerOptions, 25 | PackagingSObjects, 26 | } from '../../src/exported'; 27 | import { VersionNumber } from '../../src/package/versionNumber'; 28 | import { AncestryJsonProducer, AncestryTreeProducer, PackageAncestry } from '../../src/package/packageAncestry'; 29 | 30 | let session: TestSession; 31 | config.truncateThreshold = 0; 32 | describe('ancestry tests', () => { 33 | type PackageVersionQueryResult = PackagingSObjects.Package2Version & { 34 | Package2: { 35 | Id: string; 36 | Name: string; 37 | NamespacePrefix: string; 38 | }; 39 | }; 40 | let project: SfProject; 41 | let devHubOrg: Org; 42 | let pkgName: string; 43 | let versions: VersionNumber[]; 44 | let sortedVersions: VersionNumber[]; 45 | let aliases: { [alias: string]: string }; 46 | 47 | before('ancestry project setup', async () => { 48 | const query = 49 | "SELECT AncestorId, SubscriberPackageVersionId, MajorVersion, MinorVersion, PatchVersion, BuildNumber, Package2Id, Package2.Name, package2.NamespacePrefix FROM Package2Version where package2.containeroptions = 'Managed' AND IsReleased = true"; 50 | 51 | // will auth the hub 52 | session = await TestSession.create({ 53 | project: { 54 | sourceDir: path.join('test', 'package', 'resources', 'packageProject'), 55 | }, 56 | devhubAuthStrategy: 'AUTO', 57 | }); 58 | execCmd('config:set restDeploy=false', { ensureExitCode: 0, cli: 'sf' }); 59 | 60 | devHubOrg = await Org.create({ aliasOrUsername: session.hubOrg.username }); 61 | const queryRecords = (await devHubOrg.getConnection().tooling.query(query)).records; 62 | 63 | // preferred well known package pnhcoverage3, but if it's not available, use the first one 64 | pkgName = queryRecords.some((pv) => pv.Package2.Name === 'pnhcoverage3') 65 | ? 'pnhcoverage3' 66 | : queryRecords[0].Package2.Name; 67 | 68 | const pvRecords = queryRecords.filter((pv) => pv.Package2.Name === pkgName); 69 | versions = pvRecords.map( 70 | (pv) => new VersionNumber(pv.MajorVersion, pv.MinorVersion, pv.PatchVersion, pv.BuildNumber) 71 | ); 72 | sortedVersions = [...versions].sort((a, b) => a.compareTo(b)); 73 | project = await SfProject.resolve(); 74 | const pjson = project.getSfProjectJson(); 75 | const pkg = { 76 | ...project.getDefaultPackage(), 77 | package: pkgName, 78 | versionNumber: sortedVersions[0].toString(), 79 | versionName: 'v1', 80 | }; 81 | 82 | pjson.set('packageDirectories', [pkg]); 83 | aliases = Object.fromEntries([ 84 | ...pvRecords.map((pv, index) => [ 85 | `${pv.Package2.Name}@${versions[index].toString()}`, 86 | pv.SubscriberPackageVersionId, 87 | ]), 88 | [pkgName, pvRecords[0].Package2Id], 89 | ]) as { [alias: string]: string }; 90 | pjson.set('packageAliases', aliases); 91 | pjson.set('namespace', pvRecords[0].Package2.NamespacePrefix); 92 | await pjson.write(); 93 | }); 94 | 95 | after(async () => { 96 | await session?.clean(); 97 | }); 98 | it('should have a correct project config', async () => { 99 | expect(project.getSfProjectJson().get('packageAliases')).to.have.property(pkgName); 100 | }); 101 | it('should produce a json representation of the ancestor tree from package name (0Ho)', async () => { 102 | const pa = await PackageAncestry.create({ packageId: pkgName, project, connection: devHubOrg.getConnection() }); 103 | expect(pa).to.be.ok; 104 | const jsonProducer = pa.getRepresentationProducer( 105 | (opts: AncestryRepresentationProducerOptions) => new AncestryJsonProducer(opts), 106 | undefined 107 | ); 108 | expect(jsonProducer).to.be.ok; 109 | const jsonTree = jsonProducer.produce(); 110 | expect(jsonTree).to.have.property('data'); 111 | expect(jsonTree).to.have.property('children'); 112 | }); 113 | it('should produce a graphic representation of the ancestor tree from package name (0Ho)', async () => { 114 | const pa = await PackageAncestry.create({ packageId: pkgName, project, connection: devHubOrg.getConnection() }); 115 | expect(pa).to.be.ok; 116 | 117 | class TestAncestryTreeProducer extends AncestryTreeProducer implements AncestryRepresentationProducer { 118 | public static treeAsText: string; 119 | 120 | public constructor(options?: AncestryRepresentationProducerOptions) { 121 | super(options); 122 | } 123 | } 124 | 125 | const treeProducer = pa.getRepresentationProducer( 126 | (opts: AncestryRepresentationProducerOptions) => 127 | new TestAncestryTreeProducer({ 128 | ...opts, 129 | logger: (text: string) => (TestAncestryTreeProducer.treeAsText = text), 130 | }), 131 | undefined 132 | ); 133 | expect(treeProducer).to.be.ok; 134 | treeProducer.produce(); 135 | const treeText = TestAncestryTreeProducer.treeAsText.split(os.EOL); 136 | expect(treeText[0]).to.match(new RegExp(`^└─ ${sortedVersions[0].toString()}`)); 137 | }); 138 | it('should produce a verbose graphic representation of the ancestor tree from package name (0Ho)', async () => { 139 | const pa = await PackageAncestry.create({ packageId: pkgName, project, connection: devHubOrg.getConnection() }); 140 | expect(pa).to.be.ok; 141 | 142 | class TestAncestryTreeProducer extends AncestryTreeProducer implements AncestryRepresentationProducer { 143 | public static treeAsText: string; 144 | 145 | public constructor(options?: AncestryRepresentationProducerOptions) { 146 | super(options); 147 | } 148 | } 149 | 150 | const treeProducer = pa.getRepresentationProducer( 151 | (opts: AncestryRepresentationProducerOptions) => 152 | new TestAncestryTreeProducer({ 153 | ...opts, 154 | logger: (text: string) => (TestAncestryTreeProducer.treeAsText = text), 155 | verbose: true, 156 | }), 157 | undefined 158 | ); 159 | expect(treeProducer).to.be.ok; 160 | treeProducer.produce(); 161 | const treeText = TestAncestryTreeProducer.treeAsText.split(os.EOL); 162 | expect(treeText[0]).to.match(new RegExp(`^└─ ${sortedVersions[0].toString()} \\(04t.{12,15}\\)`)); 163 | }); 164 | it('should get path from leaf to root', async () => { 165 | const pa = await PackageAncestry.create({ packageId: pkgName, project, connection: devHubOrg.getConnection() }); 166 | expect(pa).to.be.ok; 167 | const graph = pa.getAncestryGraph(); 168 | const root = graph.findNode((n) => graph.inDegree(n) === 0); 169 | const subIds: string[] = Object.values(project.getSfProjectJson().getContents().packageAliases ?? []).filter( 170 | (id: string) => id.startsWith('04t') 171 | ); 172 | const leaf = subIds[subIds.length - 1]; 173 | const pathsToRoots = pa.getLeafPathToRoot(leaf); 174 | expect(pathsToRoots).to.be.ok; 175 | expect(pathsToRoots).to.have.lengthOf(1); 176 | expect(pathsToRoots[0][0].SubscriberPackageVersionId).to.equal(leaf); 177 | expect(pathsToRoots[0][pathsToRoots[0].length - 1].getVersion()).to.equal(root); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /src/package/packageVersionCreateRequest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import util from 'node:util'; 18 | import { Connection, Messages } from '@salesforce/core'; 19 | import type { Schema } from '@jsforce/jsforce-node'; 20 | import { 21 | PackageVersionCreateRequestQueryOptions, 22 | PackageVersionCreateRequestResult, 23 | PackagingSObjects, 24 | } from '../interfaces'; 25 | import { applyErrorAction, massageErrorMessage } from '../utils/packageUtils'; 26 | import Package2VersionCreateRequestError = PackagingSObjects.Package2VersionCreateRequestError; 27 | 28 | Messages.importMessagesDirectory(__dirname); 29 | const messages = Messages.loadMessages('@salesforce/packaging', 'package_version_create'); 30 | 31 | export function getQuery(connection: Connection): string { 32 | const QUERY = 33 | 'SELECT Id, Status, Package2Id, Package2.Name, Package2VersionId, Package2Version.SubscriberPackageVersionId, Package2Version.HasPassedCodeCoverageCheck,Package2Version.CodeCoverage, Tag, Branch, ' + 34 | 'Package2Version.MajorVersion, Package2Version.MinorVersion, Package2Version.PatchVersion, Package2Version.BuildNumber, ' + 35 | 'CreatedDate, Package2Version.HasMetadataRemoved, CreatedById, IsConversionRequest, Package2Version.ConvertedFromVersionId ' + 36 | (Number(connection.version) > 60.0 ? ', AsyncValidation ' : '') + 37 | (Number(connection.version) > 63.0 38 | ? ', Package2Version.TotalNumberOfMetadataFiles, Package2Version.TotalSizeOfMetadataFiles ' 39 | : '') + 40 | 'FROM Package2VersionCreateRequest ' + 41 | '%s' + // WHERE, if applicable 42 | 'ORDER BY CreatedDate desc'; 43 | 44 | return QUERY; 45 | } 46 | 47 | function formatDate(date: Date): string { 48 | const pad = (num: number): string => (num < 10 ? `0${num}` : `${num}`); 49 | return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad( 50 | date.getMinutes() 51 | )}`; 52 | } 53 | 54 | export async function list( 55 | connection: Connection, 56 | options?: PackageVersionCreateRequestQueryOptions 57 | ): Promise { 58 | try { 59 | const whereClause = constructWhere(options); 60 | return await query(util.format(getQuery(connection), whereClause), connection); 61 | } catch (err) { 62 | if (err instanceof Error) { 63 | throw applyErrorAction(massageErrorMessage(err)); 64 | } 65 | throw err; 66 | } 67 | } 68 | 69 | export async function byId( 70 | packageVersionCreateRequestId: string, 71 | connection: Connection 72 | ): Promise { 73 | const results = await query( 74 | util.format(getQuery(connection), `WHERE Id = '${packageVersionCreateRequestId}' `), 75 | connection 76 | ); 77 | if (results && results.length === 1 && results[0].Status === PackagingSObjects.Package2VersionStatus.error) { 78 | results[0].Error = await queryForErrors(packageVersionCreateRequestId, connection); 79 | } 80 | 81 | return results; 82 | } 83 | 84 | // eslint-disable-next-line @typescript-eslint/no-shadow 85 | async function query(query: string, connection: Connection): Promise { 86 | type QueryRecord = PackagingSObjects.Package2VersionCreateRequest & 87 | Schema & { 88 | Package2Version: Pick< 89 | PackagingSObjects.Package2Version, 90 | | 'HasMetadataRemoved' 91 | | 'SubscriberPackageVersionId' 92 | | 'ConvertedFromVersionId' 93 | | 'HasPassedCodeCoverageCheck' 94 | | 'CodeCoverage' 95 | | 'MajorVersion' 96 | | 'MinorVersion' 97 | | 'PatchVersion' 98 | | 'BuildNumber' 99 | | 'TotalNumberOfMetadataFiles' 100 | | 'TotalSizeOfMetadataFiles' 101 | >; 102 | } & { 103 | Package2: Pick; 104 | }; 105 | const queryResult = await connection.autoFetchQuery(query, { tooling: true }); 106 | return (queryResult.records ? queryResult.records : []).map((record) => ({ 107 | Id: record.Id, 108 | Status: record.Status, 109 | Package2Id: record.Package2Id, 110 | Package2Name: record.Package2 != null ? record.Package2.Name : null, 111 | Package2VersionId: record.Package2VersionId, 112 | SubscriberPackageVersionId: 113 | record.Package2Version != null ? record.Package2Version.SubscriberPackageVersionId : null, 114 | Tag: record.Tag, 115 | Branch: record.Branch, 116 | Error: [], 117 | CreatedDate: formatDate(new Date(record.CreatedDate)), 118 | HasMetadataRemoved: record.Package2Version != null ? record.Package2Version.HasMetadataRemoved : null, 119 | CodeCoverage: 120 | record.Package2Version?.CodeCoverage != null 121 | ? record.Package2Version.CodeCoverage.apexCodeCoveragePercentage 122 | : null, 123 | VersionNumber: 124 | record.Package2Version != null 125 | ? `${record.Package2Version.MajorVersion}.${record.Package2Version.MinorVersion}.${record.Package2Version.PatchVersion}.${record.Package2Version.BuildNumber}` 126 | : null, 127 | HasPassedCodeCoverageCheck: 128 | record.Package2Version != null ? record.Package2Version.HasPassedCodeCoverageCheck : null, 129 | CreatedBy: record.CreatedById, 130 | ConvertedFromVersionId: convertedFromVersionMessage(record.Status, record.Package2Version?.ConvertedFromVersionId), 131 | TotalNumberOfMetadataFiles: 132 | record.Package2Version != null ? record.Package2Version.TotalNumberOfMetadataFiles : null, 133 | TotalSizeOfMetadataFiles: record.Package2Version != null ? record.Package2Version.TotalSizeOfMetadataFiles : null, 134 | })); 135 | } 136 | 137 | function convertedFromVersionMessage(status: string, convertedFromVersionId: string): string { 138 | switch (status) { 139 | case 'Success': 140 | return convertedFromVersionId; 141 | case 'Queued': 142 | return messages.getMessage('IdUnavailableWhenQueued'); 143 | case 'InProgress': 144 | return messages.getMessage('IdUnavailableWhenInProgress'); 145 | case 'Error': 146 | return messages.getMessage('IdUnavailableWhenError'); 147 | default: 148 | return messages.getMessage('IdUnavailableWhenInProgress'); 149 | } 150 | } 151 | 152 | async function queryForErrors(packageVersionCreateRequestId: string, connection: Connection): Promise { 153 | const queryResult = await connection.tooling.query( 154 | `SELECT Message FROM Package2VersionCreateRequestError WHERE ParentRequest.Id = '${packageVersionCreateRequestId}'` 155 | ); 156 | return queryResult.records ? queryResult.records.map((record) => record.Message) : []; 157 | } 158 | 159 | function constructWhere(options?: PackageVersionCreateRequestQueryOptions): string { 160 | const where: string[] = []; 161 | 162 | if (options?.id) { 163 | where.push(`Id = '${options.id}'`); 164 | } 165 | // filter on created date, days ago: 0 for today, etc 166 | if (options?.createdlastdays !== undefined) { 167 | if (options.createdlastdays < 0) { 168 | throw messages.createError('invalidDaysNumber', ['createdlastdays', options.createdlastdays]); 169 | } 170 | where.push(`CreatedDate = LAST_N_DAYS:${options.createdlastdays}`); 171 | } 172 | 173 | // filter on errors 174 | if (options?.status) { 175 | where.push(`Status = '${options.status.toLowerCase()}'`); 176 | } 177 | 178 | // show only conversions 179 | if (options?.showConversionsOnly) { 180 | where.push('IsConversionRequest = true '); 181 | } 182 | 183 | return where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; 184 | } 185 | -------------------------------------------------------------------------------- /src/package/profileRewriter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025, Salesforce, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import type { Profile } from '@salesforce/types/metadata'; 17 | import { XMLBuilder, XMLParser } from 'fast-xml-parser'; 18 | import { PackageXml } from '../interfaces'; 19 | 20 | // TODO: NEXT MAJOR remove type, just use profile from @salesforce/types 21 | export type CorrectedProfile = Profile; 22 | 23 | /** 24 | * 25 | * Takes a Profile that's been converted from package.xml to json. 26 | * Filters out all Profile props that are not 27 | * 1. used by packaging (ex: ipRanges) 28 | * 2. present in the package.xml (ex: ClassAccesses for a class not in the package) 29 | * 3. optionally retains the UserLicense prop only if the param is true 30 | * 31 | * @param profileJson json representation of a profile 32 | * @param packageXml package.xml as json 33 | * @param retainUserLicense boolean will preserve userLicense if true 34 | * @returns Profile 35 | */ 36 | export const profileRewriter = ( 37 | profileJson: CorrectedProfile, 38 | packageXml: PackageMap, 39 | retainUserLicense = false 40 | ): CorrectedProfile => 41 | ({ 42 | ...Object.fromEntries( 43 | Object.entries(profileJson) 44 | // remove settings that are not used for packaging 45 | .filter(isRewriteProp) 46 | // @ts-expect-error the previous filter restricts us to only things that appear in filterFunctions 47 | .map(([key, value]) => [key, filterFunctions[key]?.(value, packageXml)] ?? []) 48 | // some profileSettings might now be empty Arrays if the package.xml didn't have those types, so remove the entire property 49 | .filter(([, value]) => (Array.isArray(value) ? value.length : true)) 50 | ), 51 | // this one prop is controlled by a param. Put it back the way it was if the param is true 52 | ...(retainUserLicense && profileJson.userLicense ? { userLicense: profileJson.userLicense } : {}), 53 | } as CorrectedProfile); 54 | 55 | // it's both a filter and a typeguard to make sure props are represented in filterFunctions 56 | const isRewriteProp = ( 57 | prop: [string, unknown] 58 | ): prop is [K, RewriteProps[K]] => rewriteProps.includes(prop[0] as keyof RewriteProps); 59 | 60 | const rewriteProps = [ 61 | 'objectPermissions', 62 | 'fieldPermissions', 63 | 'layoutAssignments', 64 | 'applicationVisibilities', 65 | 'classAccesses', 66 | 'externalDataSourceAccesses', 67 | 'tabVisibilities', 68 | 'pageAccesses', 69 | 'customPermissions', 70 | 'customMetadataTypeAccesses', 71 | 'customSettingAccesses', 72 | 'recordTypeVisibilities', 73 | ] as const; 74 | 75 | /** Packaging compares certain Profile properties to the package.xml */ 76 | type RewriteProps = Pick; 77 | 78 | type FilterFunction = (props: T, packageXml: PackageMap) => T; 79 | 80 | type FilterFunctions = { 81 | [index in keyof RewriteProps]: FilterFunction; 82 | }; 83 | 84 | const filterFunctions: FilterFunctions = { 85 | objectPermissions: (props: RewriteProps['objectPermissions'], packageXml: PackageMap) => 86 | props.filter((item) => packageXml.get('CustomObject')?.includes(item.object)), 87 | 88 | fieldPermissions: (props: RewriteProps['fieldPermissions'], packageXml: PackageMap) => 89 | props.filter((item) => packageXml.get('CustomField')?.includes(fieldCorrections(item.field))), 90 | 91 | layoutAssignments: (props: RewriteProps['layoutAssignments'], packageXml: PackageMap) => 92 | props.filter((item) => packageXml.get('Layout')?.includes(item.layout)), 93 | 94 | tabVisibilities: (props: RewriteProps['tabVisibilities'], packageXml: PackageMap) => 95 | props.filter((item) => packageXml.get('CustomTab')?.includes(item.tab)), 96 | 97 | applicationVisibilities: (props: RewriteProps['applicationVisibilities'], packageXml: PackageMap) => 98 | props.filter((item) => packageXml.get('CustomApplication')?.includes(item.application)), 99 | 100 | classAccesses: (props: RewriteProps['classAccesses'], packageXml: PackageMap) => 101 | props.filter((item) => packageXml.get('ApexClass')?.includes(item.apexClass)), 102 | 103 | customPermissions: (props: RewriteProps['customPermissions'], packageXml: PackageMap) => 104 | props.filter((item) => packageXml.get('CustomPermission')?.includes(item.name)), 105 | 106 | pageAccesses: (props: RewriteProps['pageAccesses'], packageXml: PackageMap) => 107 | props.filter((item) => packageXml.get('ApexPage')?.includes(item.apexPage)), 108 | 109 | externalDataSourceAccesses: (props: RewriteProps['externalDataSourceAccesses'], packageXml: PackageMap) => 110 | props.filter((item) => packageXml.get('ExternalDataSource')?.includes(item.externalDataSource)), 111 | 112 | recordTypeVisibilities: (props: RewriteProps['recordTypeVisibilities'], packageXml: PackageMap) => 113 | props.filter((item) => packageXml.get('RecordType')?.includes(item.recordType)), 114 | 115 | customSettingAccesses: (props: RewriteProps['customSettingAccesses'], packageXml: PackageMap) => 116 | props.filter((item) => allMembers(packageXml).includes(item.name)), 117 | 118 | customMetadataTypeAccesses: (props: RewriteProps['customMetadataTypeAccesses'], packageXml: PackageMap) => 119 | props.filter((item) => allMembers(packageXml).includes(item.name)), 120 | }; 121 | 122 | const allMembers = (packageXml: PackageMap): string[] => Array.from(packageXml.values()).flat(); 123 | 124 | // github.com/forcedotcom/cli/issues/2278 125 | // Activity Object is polymorphic (Task and Event) 126 | // package.xml will display them as 'Activity' 127 | // profile.fieldPermissions will display them with the more specific 'Task' or 'Event' 128 | export const fieldCorrections = (fieldName: string): string => 129 | fieldName.replace(/^Event\./, 'Activity.').replace(/^Task\./, 'Activity.'); 130 | 131 | /** 132 | * @param profileString: raw xml read from the file 133 | * @returns CorrectedProfile (json representation of the profile) 134 | */ 135 | export const profileStringToProfile = (profileString: string): CorrectedProfile => { 136 | const parser = new XMLParser({ 137 | ignoreAttributes: true, 138 | parseTagValue: false, 139 | parseAttributeValue: false, 140 | cdataPropName: '__cdata', 141 | ignoreDeclaration: true, 142 | numberParseOptions: { leadingZeros: false, hex: false }, 143 | isArray: (name: string) => rewriteProps.includes(name as keyof RewriteProps), 144 | }); 145 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion 146 | return (parser.parse(profileString) as { Profile: CorrectedProfile }).Profile; 147 | }; 148 | 149 | /** pass in an object that has the Profile props at the top level. 150 | * This function will add the outer wrapper `Profile` and convert the result to xml 151 | * */ 152 | export const profileObjectToString = (profileObject: Partial): string => { 153 | const builder = new XMLBuilder({ 154 | format: true, 155 | indentBy: ' ', 156 | ignoreAttributes: false, 157 | cdataPropName: '__cdata', 158 | processEntities: false, 159 | attributeNamePrefix: '@@@', 160 | }); 161 | return String( 162 | builder.build({ 163 | '?xml': { 164 | '@@@version': '1.0', 165 | '@@@encoding': 'UTF-8', 166 | }, 167 | Profile: { ...profileObject, '@@@xmlns': 'http://soap.sforce.com/2006/04/metadata' }, 168 | }) 169 | ); 170 | }; 171 | 172 | /** it's easier to do lookups by Metadata Type on a Map */ 173 | export const manifestTypesToMap = (original: PackageXml['types']): PackageMap => 174 | new Map(original.map((item) => [item.name, item.members])); 175 | 176 | type PackageMap = Map; 177 | --------------------------------------------------------------------------------