├── .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 | [](https://www.npmjs.com/package/@salesforce/packaging) [](https://npmjs.org/package/@salesforce/packaging) [](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 |
--------------------------------------------------------------------------------