@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
551 lines • 23.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SfdxProject = exports.SfdxProjectJson = void 0;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
const path_1 = require("path");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const configAggregator_1 = require("./config/configAggregator");
const configFile_1 = require("./config/configFile");
const validator_1 = require("./schema/validator");
const fs_1 = require("./util/fs");
const internal_1 = require("./util/internal");
const sfdxError_1 = require("./sfdxError");
const sfdc_1 = require("./util/sfdc");
const sfdcUrl_1 = require("./util/sfdcUrl");
/**
* The sfdx-project.json config object. This file determines if a folder is a valid sfdx project.
*
* *Note:* Any non-standard (not owned by Salesforce) properties stored in sfdx-project.json should
* be in a top level property that represents your project or plugin.
*
* ```
* const project = await SfdxProject.resolve();
* const projectJson = await project.resolveProjectConfig();
* const myPluginProperties = projectJson.get('myplugin') || {};
* myPluginProperties.myprop = 'someValue';
* projectJson.set('myplugin', myPluginProperties);
* await projectJson.write();
* ```
*
* **See** [force:project:create](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_create_new.htm)
*/
class SfdxProjectJson extends configFile_1.ConfigFile {
constructor(options) {
super(options);
}
static getFileName() {
return internal_1.SFDX_PROJECT_JSON;
}
static getDefaultOptions(isGlobal = false) {
const options = configFile_1.ConfigFile.getDefaultOptions(isGlobal, SfdxProjectJson.getFileName());
options.isState = false;
return options;
}
async read() {
const contents = await super.read();
this.validateKeys();
await this.schemaValidate();
return contents;
}
readSync() {
const contents = super.readSync();
this.validateKeys();
this.schemaValidateSync();
return contents;
}
async write(newContents) {
this.setContents(newContents);
this.validateKeys();
await this.schemaValidate();
return super.write();
}
writeSync(newContents) {
this.setContents(newContents);
this.validateKeys();
this.schemaValidateSync();
return super.writeSync();
}
getContents() {
return super.getContents();
}
getDefaultOptions(options) {
const defaultOptions = {
isState: false,
};
Object.assign(defaultOptions, options || {});
return defaultOptions;
}
/**
* Validates sfdx-project.json against the schema.
*
* Set the `SFDX_PROJECT_JSON_VALIDATION` environment variable to `true` to throw an error when schema validation fails.
* A warning is logged by default when the file is invalid.
*
* ***See*** [sfdx-project.schema.json] (https://github.com/forcedotcom/schemas/blob/main/sfdx-project.schema.json)
*/
async schemaValidate() {
if (!this.hasRead) {
// read calls back into this method after necessarily setting this.hasRead=true
await this.read();
}
else {
try {
const projectJsonSchemaPath = require.resolve('@salesforce/schemas/sfdx-project.schema.json');
const validator = new validator_1.SchemaValidator(this.logger, projectJsonSchemaPath);
await validator.validate(this.getContents());
}
catch (err) {
// Don't throw errors if the global isn't valid, but still warn the user.
if (kit_1.env.getBoolean('SFDX_PROJECT_JSON_VALIDATION', false) && !this.options.isGlobal) {
err.name = 'SfdxSchemaValidationError';
const sfdxError = sfdxError_1.SfdxError.wrap(err);
sfdxError.actions = [this.messages.getMessage('SchemaValidationErrorAction', [this.getPath()])];
throw sfdxError;
}
else {
this.logger.warn(this.messages.getMessage('SchemaValidationWarning', [this.getPath(), err.message]));
}
}
}
}
/**
* Returns the `packageDirectories` within sfdx-project.json, first reading
* and validating the file if necessary.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async getPackageDirectories() {
return this.getPackageDirectoriesSync();
}
/**
* Validates sfdx-project.json against the schema.
*
* Set the `SFDX_PROJECT_JSON_VALIDATION` environment variable to `true` to throw an error when schema validation fails.
* A warning is logged by default when the file is invalid.
*
* ***See*** [sfdx-project.schema.json] (https://github.com/forcedotcom/schemas/blob/main/sfdx-project.schema.json)
*/
schemaValidateSync() {
if (!this.hasRead) {
// read calls back into this method after necessarily setting this.hasRead=true
this.readSync();
}
else {
try {
const projectJsonSchemaPath = require.resolve('@salesforce/schemas/sfdx-project.schema.json');
const validator = new validator_1.SchemaValidator(this.logger, projectJsonSchemaPath);
validator.validateSync(this.getContents());
}
catch (err) {
// Don't throw errors if the global isn't valid, but still warn the user.
if (kit_1.env.getBoolean('SFDX_PROJECT_JSON_VALIDATION', false) && !this.options.isGlobal) {
err.name = 'SfdxSchemaValidationError';
const sfdxError = sfdxError_1.SfdxError.wrap(err);
sfdxError.actions = [this.messages.getMessage('SchemaValidationErrorAction', [this.getPath()])];
throw sfdxError;
}
else {
this.logger.warn(this.messages.getMessage('SchemaValidationWarning', [this.getPath(), err.message]));
}
}
}
}
/**
* Returns a read-only list of `packageDirectories` within sfdx-project.json, first reading
* and validating the file if necessary. i.e. modifying this array will not affect the
* sfdx-project.json file.
*/
getPackageDirectoriesSync() {
const contents = this.getContents();
// This has to be done on the fly so it won't be written back to the file
// This is a fast operation so no need to cache it so it stays immutable.
const packageDirs = (contents.packageDirectories || []).map((packageDir) => {
if (path_1.isAbsolute(packageDir.path)) {
throw new sfdxError_1.SfdxError('InvalidProjectWorkspace', this.messages.getMessage('InvalidAbsolutePath', [packageDir.path]));
}
const regex = path_1.sep === '/' ? /\\/g : /\//g;
// Change packageDir paths to have path separators that match the OS
const path = packageDir.path.replace(regex, path_1.sep);
// Normalize and remove any ending path separators
const name = path_1.normalize(path).replace(new RegExp(`\\${path_1.sep}$`), '');
// Always end in a path sep for standardization on folder paths
const fullPath = `${path_1.dirname(this.getPath())}${path_1.sep}${name}${path_1.sep}`;
if (!this.doesPackageExist(fullPath)) {
throw new sfdxError_1.SfdxError('InvalidPackageDirectory', this.messages.getMessage('InvalidPackageDirectory', [packageDir.path]));
}
return Object.assign({}, packageDir, { name, path, fullPath });
});
// If we only have one package entry, it must be the default even if not explicitly labelled
if (packageDirs.length === 1) {
if (packageDirs[0].default === false) {
// we have one package but it is explicitly labelled as default=false
throw new sfdxError_1.SfdxError(this.messages.getMessage('SingleNonDefaultPackage'));
}
// add default=true to the package
packageDirs[0].default = true;
}
const defaultDirs = packageDirs.filter((packageDir) => packageDir.default);
// Don't throw about a missing default path if we are in the global file.
// Package directories are not really meant to be set at the global level.
if (defaultDirs.length === 0 && !this.isGlobal()) {
throw new sfdxError_1.SfdxError(this.messages.getMessage('MissingDefaultPath'));
}
else if (defaultDirs.length > 1) {
throw new sfdxError_1.SfdxError(this.messages.getMessage('MultipleDefaultPaths'));
}
return packageDirs;
}
/**
* Returns a read-only list of `packageDirectories` within sfdx-project.json, first reading
* and validating the file if necessary. i.e. modifying this array will not affect the
* sfdx-project.json file.
*
* There can be multiple packages in packageDirectories that point to the same directory.
* This method only returns one packageDirectory entry per unique directory path. This is
* useful when doing source operations based on directories but probably not as useful
* for packaging operations that want to do something for each package entry.
*/
getUniquePackageDirectories() {
const visited = {};
const uniqueValues = [];
// Keep original order defined in sfdx-project.json
this.getPackageDirectoriesSync().forEach((packageDir) => {
if (!visited[packageDir.name]) {
visited[packageDir.name] = true;
uniqueValues.push(packageDir);
}
});
return uniqueValues;
}
/**
* Get a list of the unique package names from within sfdx-project.json. Use {@link SfdxProject.getUniquePackageDirectories}
* for data other than the names.
*/
getUniquePackageNames() {
return this.getUniquePackageDirectories().map((pkgDir) => pkgDir.name);
}
/**
* Has package directories defined in the project.
*/
hasPackages() {
return this.getContents().packageDirectories && this.getContents().packageDirectories.length > 0;
}
/**
* Has multiple package directories (MPD) defined in the project.
*/
hasMultiplePackages() {
return this.getContents().packageDirectories && this.getContents().packageDirectories.length > 1;
}
doesPackageExist(packagePath) {
return fs_1.fs.existsSync(packagePath);
}
validateKeys() {
// Verify that the configObject does not have upper case keys; throw if it does. Must be heads down camel case.
const upperCaseKey = sfdc_1.sfdc.findUpperCaseKeys(this.toObject(), SfdxProjectJson.BLOCKLIST);
if (upperCaseKey) {
throw sfdxError_1.SfdxError.create('@salesforce/core', 'core', 'InvalidJsonCasing', [upperCaseKey, this.getPath()]);
}
}
}
exports.SfdxProjectJson = SfdxProjectJson;
SfdxProjectJson.BLOCKLIST = ['packageAliases'];
/**
* Represents an SFDX project directory. This directory contains a {@link SfdxProjectJson} config file as well as
* a hidden .sfdx folder that contains all the other local project config files.
*
* ```
* const project = await SfdxProject.resolve();
* const projectJson = await project.resolveProjectConfig();
* console.log(projectJson.sfdcLoginUrl);
* ```
*/
class SfdxProject {
/**
* Do not directly construct instances of this class -- use {@link SfdxProject.resolve} instead.
*
* @ignore
*/
constructor(path) {
this.path = path;
}
/**
* Get a Project from a given path or from the working directory.
*
* @param path The path of the project.
*
* **Throws** *{@link SfdxError}{ name: 'InvalidProjectWorkspace' }* If the current folder is not located in a workspace.
*/
static async resolve(path) {
path = await this.resolveProjectPath(path || process.cwd());
if (!SfdxProject.instances.has(path)) {
const project = new SfdxProject(path);
SfdxProject.instances.set(path, project);
}
return ts_types_1.ensure(SfdxProject.instances.get(path));
}
/**
* Get a Project from a given path or from the working directory.
*
* @param path The path of the project.
*
* **Throws** *{@link SfdxError}{ name: 'InvalidProjectWorkspace' }* If the current folder is not located in a workspace.
*/
static getInstance(path) {
// Store instance based on the path of the actual project.
path = this.resolveProjectPathSync(path || process.cwd());
if (!SfdxProject.instances.has(path)) {
const project = new SfdxProject(path);
SfdxProject.instances.set(path, project);
}
return ts_types_1.ensure(SfdxProject.instances.get(path));
}
/**
* Performs an upward directory search for an sfdx project file. Returns the absolute path to the project.
*
* @param dir The directory path to start traversing from.
*
* **Throws** *{@link SfdxError}{ name: 'InvalidProjectWorkspace' }* If the current folder is not located in a workspace.
*
* **See** {@link traverseForFile}
*
* **See** [process.cwd()](https://nodejs.org/api/process.html#process_process_cwd)
*/
static async resolveProjectPath(dir) {
return internal_1.resolveProjectPath(dir);
}
/**
* Performs a synchronous upward directory search for an sfdx project file. Returns the absolute path to the project.
*
* @param dir The directory path to start traversing from.
*
* **Throws** *{@link SfdxError}{ name: 'InvalidProjectWorkspace' }* If the current folder is not located in a workspace.
*
* **See** {@link traverseForFileSync}
*
* **See** [process.cwd()](https://nodejs.org/api/process.html#process_process_cwd)
*/
static resolveProjectPathSync(dir) {
return internal_1.resolveProjectPathSync(dir);
}
/**
* Returns the project path.
*/
getPath() {
return this.path;
}
/**
* Get the sfdx-project.json config. The global sfdx-project.json is used for user defaults
* that are not checked in to the project specific file.
*
* *Note:* When reading values from {@link SfdxProjectJson}, it is recommended to use
* {@link SfdxProject.resolveProjectConfig} instead.
*
* @param isGlobal True to get the global project file, otherwise the local project config.
*/
async retrieveSfdxProjectJson(isGlobal = false) {
const options = SfdxProjectJson.getDefaultOptions(isGlobal);
if (isGlobal) {
if (!this.sfdxProjectJsonGlobal) {
this.sfdxProjectJsonGlobal = await SfdxProjectJson.create(options);
}
return this.sfdxProjectJsonGlobal;
}
else {
options.rootFolder = this.getPath();
if (!this.sfdxProjectJson) {
this.sfdxProjectJson = await SfdxProjectJson.create(options);
}
return this.sfdxProjectJson;
}
}
/**
* Get the sfdx-project.json config. The global sfdx-project.json is used for user defaults
* that are not checked in to the project specific file.
*
* *Note:* When reading values from {@link SfdxProjectJson}, it is recommended to use
* {@link SfdxProject.resolveProjectConfig} instead.
*
* This is the sync method of {@link SfdxProject.resolveSfdxProjectJson}
*
* @param isGlobal True to get the global project file, otherwise the local project config.
*/
getSfdxProjectJson(isGlobal = false) {
const options = SfdxProjectJson.getDefaultOptions(isGlobal);
if (isGlobal) {
if (!this.sfdxProjectJsonGlobal) {
this.sfdxProjectJsonGlobal = new SfdxProjectJson(options);
this.sfdxProjectJsonGlobal.readSync();
}
return this.sfdxProjectJsonGlobal;
}
else {
options.rootFolder = this.getPath();
if (!this.sfdxProjectJson) {
this.sfdxProjectJson = new SfdxProjectJson(options);
this.sfdxProjectJson.readSync();
}
return this.sfdxProjectJson;
}
}
/**
* Returns a read-only list of `packageDirectories` within sfdx-project.json, first reading
* and validating the file if necessary. i.e. modifying this array will not affect the
* sfdx-project.json file.
*/
getPackageDirectories() {
if (!this.packageDirectories) {
this.packageDirectories = this.getSfdxProjectJson().getPackageDirectoriesSync();
}
return this.packageDirectories;
}
/**
* Returns a read-only list of `packageDirectories` within sfdx-project.json, first reading
* and validating the file if necessary. i.e. modifying this array will not affect the
* sfdx-project.json file.
*
* There can be multiple packages in packageDirectories that point to the same directory.
* This method only returns one packageDirectory entry per unique directory path. This is
* useful when doing source operations based on directories but probably not as useful
* for packaging operations that want to do something for each package entry.
*/
getUniquePackageDirectories() {
return this.getSfdxProjectJson().getUniquePackageDirectories();
}
/**
* Get a list of the unique package names from within sfdx-project.json. Use {@link SfdxProject.getUniquePackageDirectories}
* for data other than the names.
*/
getUniquePackageNames() {
return this.getSfdxProjectJson().getUniquePackageNames();
}
/**
* Returns the package from a file path.
*
* @param path A file path. E.g. /Users/jsmith/projects/ebikes-lwc/force-app/apex/my-cls.cls
*/
getPackageFromPath(path) {
const packageDirs = this.getPackageDirectories();
const match = packageDirs.find((packageDir) => path_1.basename(path) === packageDir.name || path.includes(packageDir.fullPath));
return match;
}
/**
* Returns the package name, E.g. 'force-app', from a file path.
*
* @param path A file path. E.g. /Users/jsmith/projects/ebikes-lwc/force-app/apex/my-cls.cls
*/
getPackageNameFromPath(path) {
const packageDir = this.getPackageFromPath(path);
return packageDir ? packageDir.name : undefined;
}
/**
* Returns the package directory.
*
* @param packageName Name of the package directory. E.g., 'force-app'
*/
getPackage(packageName) {
const packageDirs = this.getPackageDirectories();
return packageDirs.find((packageDir) => packageDir.name === packageName);
}
/**
* Returns the absolute path of the package directory ending with the path separator.
* E.g., /Users/jsmith/projects/ebikes-lwc/force-app/
*
* @param packageName Name of the package directory. E.g., 'force-app'
*/
getPackagePath(packageName) {
const packageDir = this.getPackage(packageName);
return packageDir && packageDir.fullPath;
}
/**
* Has package directories defined in the project.
*/
hasPackages() {
return this.getSfdxProjectJson().hasPackages();
}
/**
* Has multiple package directories (MPD) defined in the project.
*/
hasMultiplePackages() {
return this.getSfdxProjectJson().hasMultiplePackages();
}
/**
* Get the currently activated package on the project. This has no implication on sfdx-project.json
* but is useful for keeping track of package and source specific options in a process.
*/
getActivePackage() {
return this.activePackage;
}
/**
* Set the currently activated package on the project. This has no implication on sfdx-project.json
* but is useful for keeping track of package and source specific options in a process.
*
* @param pkgName The package name to activate. E.g. 'force-app'
*/
setActivePackage(packageName) {
if (packageName == null) {
this.activePackage = null;
}
else {
this.activePackage = this.getPackage(packageName);
}
}
/**
* Get the project's default package directory defined in sfdx-project.json using first 'default: true'
* found. The first entry is returned if no default is specified.
*/
getDefaultPackage() {
if (!this.hasPackages()) {
throw new sfdxError_1.SfdxError('The sfdx-project.json does not have any packageDirectories defined.');
}
const defaultPackage = this.getPackageDirectories().find((packageDir) => packageDir.default === true);
return defaultPackage || this.getPackageDirectories()[0];
}
/**
* The project config is resolved from local and global {@link SfdxProjectJson},
* {@link ConfigAggregator}, and a set of defaults. It is recommended to use
* this when reading values from SfdxProjectJson.
*
* The global {@link SfdxProjectJson} is used to allow the user to provide default values they
* may not want checked into their project's source.
*
* @returns A resolved config object that contains a bunch of different
* properties, including some 3rd party custom properties.
*/
async resolveProjectConfig() {
var _a;
if (!this.projectConfig) {
// Do fs operations in parallel
const [global, local, configAggregator] = await Promise.all([
this.retrieveSfdxProjectJson(true),
this.retrieveSfdxProjectJson(),
configAggregator_1.ConfigAggregator.create(),
]);
await Promise.all([global.read(), local.read()]);
this.projectConfig = kit_1.defaults(local.toObject(), global.toObject());
// Add fields in sfdx-config.json
Object.assign(this.projectConfig, configAggregator.getConfig());
// we don't have a login url yet, so use instanceUrl from config or default
if (!this.projectConfig.sfdcLoginUrl) {
this.projectConfig.sfdcLoginUrl = (_a = configAggregator.getConfig().instanceUrl) !== null && _a !== void 0 ? _a : sfdcUrl_1.SfdcUrl.PRODUCTION;
}
// LEGACY - Allow override of sfdcLoginUrl via env var FORCE_SFDC_LOGIN_URL
if (process.env.FORCE_SFDC_LOGIN_URL) {
this.projectConfig.sfdcLoginUrl = process.env.FORCE_SFDC_LOGIN_URL;
}
// Allow override of signupTargetLoginUrl via env var SFDX_SCRATCH_ORG_CREATION_LOGIN_URL
if (process.env.SFDX_SCRATCH_ORG_CREATION_LOGIN_URL) {
this.projectConfig.signupTargetLoginUrl = process.env.SFDX_SCRATCH_ORG_CREATION_LOGIN_URL;
}
}
return this.projectConfig;
}
}
exports.SfdxProject = SfdxProject;
// Cache of SfdxProject instances per path.
SfdxProject.instances = new Map();
//# sourceMappingURL=sfdxProject.js.map