UNPKG

@salesforce/core

Version:

Core libraries to interact with SFDX projects, orgs, and APIs.

551 lines 23.7 kB
"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