diff --git a/README.md b/README.md index 88630feb..f41ed491 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ FLAG DESCRIPTIONS sandbox. ``` -_See code: [lib/commands/org/create/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/create/sandbox.ts)_ +_See code: [src/commands/org/create/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/create/sandbox.ts)_ ## `sf org create scratch` @@ -236,7 +236,8 @@ FLAGS -i, --client-id= Consumer key of the Dev Hub connected app. -t, --[no-]track-source Use source tracking for this scratch org. Set --no-track-source to disable source tracking. - -v, --target-dev-hub= (required) Username or alias of the Dev Hub org. + -v, --target-dev-hub= (required) [default: admin@integrationtesthubna40.org] Username or alias of the Dev Hub + org. -w, --wait= [default: 5 minutes] Number of minutes to wait for the scratch org to be ready. -y, --duration-days= [default: 7 days] Number of days before the org expires. --api-version= Override the api version used for api requests made by this command @@ -369,7 +370,7 @@ FLAG DESCRIPTIONS Omit this flag to have Salesforce generate a unique username for your org. ``` -_See code: [lib/commands/org/create/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/create/scratch.ts)_ +_See code: [src/commands/org/create/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/create/scratch.ts)_ ## `sf org delete sandbox` @@ -413,7 +414,7 @@ EXAMPLES $ sf org delete sandbox --target-org my-sandbox --no-prompt ``` -_See code: [lib/commands/org/delete/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/delete/sandbox.ts)_ +_See code: [src/commands/org/delete/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/delete/sandbox.ts)_ ## `sf org delete scratch` @@ -455,7 +456,7 @@ EXAMPLES $ sf org delete scratch --target-org my-scratch-org --no-prompt ``` -_See code: [lib/commands/org/delete/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/delete/scratch.ts)_ +_See code: [src/commands/org/delete/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/delete/scratch.ts)_ ## `sf org disable tracking` @@ -493,7 +494,7 @@ EXAMPLES $ sf org disable tracking ``` -_See code: [lib/commands/org/disable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/disable/tracking.ts)_ +_See code: [src/commands/org/disable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/disable/tracking.ts)_ ## `sf org display` @@ -537,7 +538,7 @@ EXAMPLES $ sf org display --target-org TestOrg1 --verbose ``` -_See code: [lib/commands/org/display.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/display.ts)_ +_See code: [src/commands/org/display.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/display.ts)_ ## `sf org enable tracking` @@ -578,7 +579,7 @@ EXAMPLES $ sf org enable tracking ``` -_See code: [lib/commands/org/enable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/enable/tracking.ts)_ +_See code: [src/commands/org/enable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/enable/tracking.ts)_ ## `sf org list` @@ -616,7 +617,7 @@ EXAMPLES $ sf org list --clean ``` -_See code: [lib/commands/org/list.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/list.ts)_ +_See code: [src/commands/org/list.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/list.ts)_ ## `sf org list metadata` @@ -681,7 +682,7 @@ FLAG DESCRIPTIONS Examples of metadata types that use folders are Dashboard, Document, EmailTemplate, and Report. ``` -_See code: [lib/commands/org/list/metadata.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/list/metadata.ts)_ +_See code: [src/commands/org/list/metadata.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/list/metadata.ts)_ ## `sf org list metadata-types` @@ -735,7 +736,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [lib/commands/org/list/metadata-types.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/list/metadata-types.ts)_ +_See code: [src/commands/org/list/metadata-types.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/list/metadata-types.ts)_ ## `sf org open` @@ -801,7 +802,7 @@ EXAMPLES $ sf org open --source-file force-app/main/default/flows/Hello.flow-meta.xml ``` -_See code: [lib/commands/org/open.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/open.ts)_ +_See code: [src/commands/org/open.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/open.ts)_ ## `sf org resume sandbox` @@ -863,7 +864,7 @@ FLAG DESCRIPTIONS returns the job ID. To resume checking the sandbox creation, rerun this command. ``` -_See code: [lib/commands/org/resume/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/resume/sandbox.ts)_ +_See code: [src/commands/org/resume/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/resume/sandbox.ts)_ ## `sf org resume scratch` @@ -909,6 +910,6 @@ FLAG DESCRIPTIONS The job ID is valid for 24 hours after you start the scratch org creation. ``` -_See code: [lib/commands/org/resume/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.2/lib/commands/org/resume/scratch.ts)_ +_See code: [src/commands/org/resume/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/3.2.1/src/commands/org/resume/scratch.ts)_ diff --git a/command-snapshot.json b/command-snapshot.json index 1a36268b..9b7a6a81 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -177,7 +177,7 @@ "command": "org:open", "flagAliases": ["apiversion", "sourcefile", "targetusername", "u", "urlonly"], "flagChars": ["b", "f", "o", "p", "r"], - "flags": ["api-version", "browser", "json", "loglevel", "path", "source-file", "target-org", "url-only"], + "flags": ["api-version", "browser", "json", "loglevel", "path", "private", "source-file", "target-org", "url-only"], "plugin": "@salesforce/plugin-org" }, { diff --git a/messages/open.md b/messages/open.md index 7ac847b2..912a05a2 100644 --- a/messages/open.md +++ b/messages/open.md @@ -18,6 +18,10 @@ To open in a specific browser, use the --browser flag. Supported browsers are "c $ <%= config.bin %> <%= command.id %> +- Open your default org in an incognito window of your default browser: + + $ <%= config.bin %> <%= command.id %> --private + - Open the org with alias MyTestOrg1 in the Firefox browser: $ <%= config.bin %> <%= command.id %> --target-org MyTestOrg1 --browser firefox @@ -34,6 +38,10 @@ To open in a specific browser, use the --browser flag. Supported browsers are "c $ <%= config.bin %> <%= command.id %> --source-file force-app/main/default/flows/Hello.flow-meta.xml +# flags.private.summary + +Open the org in the default browser using private (incognito) mode. + # flags.browser.summary Browser where the org opens. diff --git a/package.json b/package.json index 1d65816e..bdcf3f71 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "@salesforce/source-deploy-retrieve": "^10.2.5", "chalk": "^5.3.0", "change-case": "^5.3.0", - "open": "^10.0.2" + "is-wsl": "^3.1.0", + "open": "^10.0.3" }, "devDependencies": { "@oclif/plugin-command-snapshot": "^5.0.5", @@ -23,7 +24,7 @@ "@types/shelljs": "^0.8.14", "eslint-plugin-sf-plugin": "^1.17.0", "moment": "^2.30.1", - "oclif": "^4.1.3", + "oclif": "^4.1.4", "shelljs": "^0.8.5", "shx": "0.3.4", "ts-node": "^10.9.2", diff --git a/src/commands/org/open.ts b/src/commands/org/open.ts index c7fc2e88..2482c12c 100644 --- a/src/commands/org/open.ts +++ b/src/commands/org/open.ts @@ -6,6 +6,8 @@ */ import path from 'node:path'; +import { platform, tmpdir } from 'node:os'; +import fs from 'node:fs'; import { Flags, loglevel, @@ -13,15 +15,15 @@ import { requiredOrgFlagWithDeprecations, SfCommand, } from '@salesforce/sf-plugins-core'; +import isWsl from 'is-wsl'; import { Connection, Logger, Messages, Org, SfdcUrl, SfError } from '@salesforce/core'; -import { Duration, Env } from '@salesforce/kit'; +import { Duration, Env, sleep } from '@salesforce/kit'; import { MetadataResolver } from '@salesforce/source-deploy-retrieve'; import { apps } from 'open'; import utils from '../../shared/utils.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); -const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); export class OrgOpenCommand extends SfCommand { public static readonly summary = messages.getMessage('summary'); @@ -33,11 +35,15 @@ export class OrgOpenCommand extends SfCommand { public static readonly flags = { 'target-org': requiredOrgFlagWithDeprecations, 'api-version': orgApiVersionFlagWithDeprecations, + private: Flags.boolean({ + summary: messages.getMessage('flags.private.summary'), + exclusive: ['url-only', 'browser'], + }), browser: Flags.option({ char: 'b', summary: messages.getMessage('flags.browser.summary'), options: ['chrome', 'edge', 'firefox'] as const, // These are ones supported by "open" package - exclusive: ['url-only'], + exclusive: ['url-only', 'private'], })(), path: Flags.string({ char: 'p', @@ -62,33 +68,28 @@ export class OrgOpenCommand extends SfCommand { }), }; - private org!: Org; - private conn!: Connection; - public async run(): Promise { const { flags } = await this.parse(OrgOpenCommand); + const conn = flags['target-org'].getConnection(flags['api-version']); - this.org = flags['target-org']; - this.conn = this.org.getConnection(flags['api-version']); - - let url = await this.buildFrontdoorUrl(); const env = new Env(); + const [frontDoorUrl, retUrl] = await Promise.all([ + buildFrontdoorUrl(flags['target-org'], conn), + flags['source-file'] ? generateFileUrl(flags['source-file'], conn) : flags.path, + ]); - if (flags['source-file']) { - url += `&retURL=${await this.generateFileUrl(flags['source-file'])}`; - } else if (flags.path) { - url += `&retURL=${flags.path}`; - } + const url = `${frontDoorUrl}${retUrl ? `&retURL=${retUrl}` : ''}`; - const orgId = this.org.getOrgId(); + const orgId = flags['target-org'].getOrgId(); // TODO: better typings in sfdx-core for orgs read from auth files - const username = this.org.getUsername() as string; + const username = flags['target-org'].getUsername() as string; const output = { orgId, url, username }; // NOTE: Deliberate use of `||` here since getBoolean() defaults to false, and we need to consider both env vars. const containerMode = env.getBoolean('SF_CONTAINER_MODE') || env.getBoolean('SFDX_CONTAINER_MODE'); // security warning only for --json OR --url-only OR containerMode if (flags['url-only'] || Boolean(flags.json) || containerMode) { + const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); this.warn(sharedMessages.getMessage('SecurityWarning')); this.log(''); } @@ -110,8 +111,7 @@ export class OrgOpenCommand extends SfCommand { // we actually need to open the org try { this.spinner.start(messages.getMessage('domainWaiting')); - const sfdcUrl = new SfdcUrl(url); - await sfdcUrl.checkLightningDomain(); + await new SfdcUrl(url).checkLightningDomain(); this.spinner.stop(); } catch (err) { if (err instanceof Error) { @@ -128,47 +128,23 @@ export class OrgOpenCommand extends SfCommand { throw err; } - await utils.openUrl(url, { + // create a local html file that contains the POST stuff. + const tempFilePath = path.join(tmpdir(), `org-open-${new Date().valueOf()}.html`); + await fs.promises.writeFile(tempFilePath, getFileContents(conn.accessToken as string, conn.instanceUrl, retUrl)); + const cp = await utils.openUrl(`file:///${tempFilePath}`, { ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), - // only applies on macOS. open library ignores this property as it is the default behavior on other OS - newInstance: true, + ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), }); - return output; - } - - private async buildFrontdoorUrl(): Promise { - await this.org.refreshAuth(); // we need a live accessToken for the frontdoor url - const accessToken = this.conn.accessToken; - const instanceUrl = this.org.getField(Org.Fields.INSTANCE_URL); - const instanceUrlClean = instanceUrl.replace(/\/$/, ''); - return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`; - } + cp.on('error', (err) => { + fileCleanup(tempFilePath); + throw SfError.wrap(err); + }); + // so we don't delete the file while the browser is still using it + // open returns when the CP is spawned, but there's not way to know if the browser is still using the file + await sleep(platform() === 'win32' || isWsl ? 7000 : 5000); + fileCleanup(tempFilePath); - private async generateFileUrl(file: string): Promise { - try { - const metadataResolver = new MetadataResolver(); - const components = metadataResolver.getComponentsFromPath(file); - const typeName = components[0]?.type?.name; - - if (typeName === 'FlexiPage') { - const flexipage = await this.conn.singleRecordQuery<{ Id: string }>( - `SELECT id FROM flexipage WHERE DeveloperName='${path.basename(file, '.flexipage-meta.xml')}'`, - { tooling: true } - ); - return `/visualEditor/appBuilder.app?pageId=${flexipage.Id}`; - } else if (typeName === 'ApexPage') { - return `/apex/${path.basename(file).replace('.page-meta.xml', '').replace('.page', '')}`; - } else if (typeName === 'Flow') { - return `/builder_platform_interaction/flowBuilder.app?flowId=${await flowFileNameToId(this.conn, file)}`; - } else { - return 'lightning/setup/FlexiPageList/home'; - } - } catch (error) { - if (error instanceof Error && error.name === 'FlowIdNotFoundError') { - this.error(error); - } - return 'lightning/setup/FlexiPageList/home'; - } + return output; } } @@ -178,6 +154,50 @@ export interface OrgOpenOutput { orgId: string; } +const fileCleanup = (tempFilePath: string): void => + fs.rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true }); + +const buildFrontdoorUrl = async (org: Org, conn: Connection): Promise => { + await org.refreshAuth(); // we need a live accessToken for the frontdoor url + const accessToken = conn.accessToken; + const instanceUrl = org.getField(Org.Fields.INSTANCE_URL); + const instanceUrlClean = instanceUrl.replace(/\/$/, ''); + return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`; +}; + +const generateFileUrl = async (file: string, conn: Connection): Promise => { + try { + const metadataResolver = new MetadataResolver(); + const components = metadataResolver.getComponentsFromPath(file); + const typeName = components[0]?.type?.name; + + switch (typeName) { + case 'ApexPage': + return `/apex/${path.basename(file).replace('.page-meta.xml', '').replace('.page', '')}`; + case 'Flow': + return `/builder_platform_interaction/flowBuilder.app?flowId=${await flowFileNameToId(conn, file)}`; + case 'FlexiPage': + return `/visualEditor/appBuilder.app?pageId=${await flexiPageFilenameToId(conn, file)}`; + default: + return 'lightning/setup/FlexiPageList/home'; + } + } catch (error) { + if (error instanceof Error && error.name === 'FlowIdNotFoundError') { + throw error; + } + return 'lightning/setup/FlexiPageList/home'; + } +}; + +/** query flexipage via toolingPAI to get its ID (starts with 0M0) */ +const flexiPageFilenameToId = async (conn: Connection, filePath: string): Promise => + ( + await conn.singleRecordQuery<{ Id: string }>( + `SELECT id FROM flexipage WHERE DeveloperName='${path.basename(filePath, '.flexipage-meta.xml')}'`, + { tooling: true } + ) + ).Id; + /** query the rest API to turn a flow's filepath into a FlowId (starts with 301) */ const flowFileNameToId = async (conn: Connection, filePath: string): Promise => { try { @@ -192,3 +212,19 @@ const flowFileNameToId = async (conn: Connection, filePath: string): Promise ` + + +
+ + +
+ +`; diff --git a/test/unit/org/open.test.ts b/test/unit/org/open.test.ts index a0d101c3..a81940b8 100644 --- a/test/unit/org/open.test.ts +++ b/test/unit/org/open.test.ts @@ -4,10 +4,9 @@ * 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 */ - +import { EventEmitter } from 'node:events'; import fs from 'node:fs'; import { join } from 'node:path'; - import { assert, expect } from 'chai'; import { MyDomainResolver, Messages, Connection, SfError } from '@salesforce/core'; import { Config } from '@oclif/core'; @@ -45,7 +44,7 @@ describe('org:open', () => { stubUx($$.SANDBOX); stubSpinner($$.SANDBOX); await $$.stubAuths(testOrg); - spies.set('open', stubMethod($$.SANDBOX, utils, 'openUrl').resolves()); + spies.set('open', stubMethod($$.SANDBOX, utils, 'openUrl').resolves(new EventEmitter())); }); afterEach(() => { @@ -266,7 +265,7 @@ describe('org:open', () => { expect(spies.get('resolver').callCount).to.equal(1); expect(spies.get('open').callCount).to.equal(1); - expect(spies.get('open').args[0][1]).to.deep.equal({ newInstance: true }); + expect(spies.get('open').args[0][1]).to.deep.equal({}); }); it('calls open with a browser argument', async () => { diff --git a/yarn.lock b/yarn.lock index 6b836278..a90e0af8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6102,10 +6102,10 @@ object.values@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" -oclif@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/oclif/-/oclif-4.1.3.tgz#07d3a6b16ff7e2e7c3fd06117fd745655c44abce" - integrity sha512-9OmzHaOmn58BZZt8S2USh1gW5RFfa69cDfOtXCWUyYrcRdO4wIJ3DPb0xZ3UxHHbPZKsDImuP/A54weFMH2izw== +oclif@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/oclif/-/oclif-4.1.4.tgz#661f8cae3a2b42c33058722c356e0a406cc90ad7" + integrity sha512-tgZTmJqwakJAU+7FokbhaYcj6Eaj/mVvCfeuitP6qQOzAC1s8yhkY7gZ7DsAzAEh9VqT4h4xZmTJxV21Lc8CLQ== dependencies: "@oclif/core" "^3.16.0" "@oclif/plugin-help" "^6.0.9" @@ -6145,10 +6145,10 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -open@^10.0.2: - version "10.0.2" - resolved "https://registry.yarnpkg.com/open/-/open-10.0.2.tgz#93b099c6194552b47afac969c9b83f874aee1ab8" - integrity sha512-GnYLdE+E3K8NeSE23N0g67/9q9AXRph5oTUbz6IbIgElPigEnQ2aHuqRge3y0JUr67qoc84xME5kF03fDc3fcA== +open@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/open/-/open-10.0.3.tgz#f60d8db49fa126c50aec751957fb5d7de3308d4f" + integrity sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A== dependencies: default-browser "^5.2.1" define-lazy-prop "^3.0.0"