From 9a48e5c0c036a8c7d82a5c8949dd74ec54c3a9a5 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Wed, 15 May 2024 16:00:36 -0500 Subject: [PATCH 01/20] feat(wip): decompose command --- messages/project.decompose.md | 31 ++++++ src/commands/project/decompose.ts | 136 +++++++++++++++++++++++++ test/commands/project/decompose.nut.ts | 27 +++++ yarn.lock | 31 +----- 4 files changed, 197 insertions(+), 28 deletions(-) create mode 100644 messages/project.decompose.md create mode 100644 src/commands/project/decompose.ts create mode 100644 test/commands/project/decompose.nut.ts diff --git a/messages/project.decompose.md b/messages/project.decompose.md new file mode 100644 index 00000000..c3dd584f --- /dev/null +++ b/messages/project.decompose.md @@ -0,0 +1,31 @@ +# summary + +Enable a preset in sfdx-project.json and update your project source to use it. + +# description + +Makes local changes to your project based on the chosen preset. + +# flags.preset.summary + +Which preset to enable. + +# examples + +- Switch the project to use decomposed custom labels + <%= config.bin %> <%= command.id %> --preset DecomposeCustomLabels --source-dir . + +- Switch one packageDirectory to use decomposed custom labels + <%= config.bin %> <%= command.id %> --preset DecomposeCustomLabels --source-dir force-app + +# flags.dry-run.summary + +Explain what the command would do but don't modify the project. + +# flags.preserve-temp-dir.summary + +Don't delete the metadata API format temp dir that this command creates. Useful for debugging. + +# flags.source-dir.summary + +Directory to modify the decomposition for. Can be an entire SfdxProject or any subfolder inside it. diff --git a/src/commands/project/decompose.ts b/src/commands/project/decompose.ts new file mode 100644 index 00000000..4a23c1dc --- /dev/null +++ b/src/commands/project/decompose.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023, 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 + */ + +import { readdir, readFile, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { Messages, SfError, SfProject, SfProjectJson } from '@salesforce/core'; +import { + ComponentSet, + ComponentSetBuilder, + MetadataConverter, + MetadataRegistry, +} from '@salesforce/source-deploy-retrieve'; +import { isString } from '@salesforce/ts-types'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'project.decompose'); + +export type ProjectDecomposeResult = { + presets: string[]; +}; + +// TODO: there must be a cleaner way to read this +const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( + 'file:', + '' +); +const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); +const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; + +export default class ProjectDecompose extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + public static readonly state = 'beta'; + public static readonly requiresProject = true; + + public static readonly flags = { + preset: Flags.option({ + summary: messages.getMessage('flags.preset.summary'), + char: 'p', + required: true, + options: PRESET_CHOICES, + })(), + 'dry-run': Flags.boolean({ + summary: messages.getMessage('flags.dry-run.summary'), + }), + 'preserve-temp-dir': Flags.boolean({ + summary: messages.getMessage('flags.preserve-temp-dir.summary'), + }), + 'source-dir': Flags.directory({ + summary: messages.getMessage('flags.source-dir.summary'), + char: 'd', + required: true, + multiple: true, + exists: true, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(ProjectDecompose); + const projectJson = getValidatedProjectJson(flags.preset, this.project); + const typesFromPreset = Object.values( + (JSON.parse(await readFile(join(PRESET_DIR, `${flags.preset}.json`), 'utf-8')) as MetadataRegistry).types + ).map((t) => t.name); + + const cs = await ComponentSetBuilder.build({ + metadata: { + metadataEntries: typesFromPreset, + directoryPaths: this.project!.getPackageDirectories().map((pd) => pd.path), + }, + }); + + await new MetadataConverter().convert(cs, 'metadata', { + type: 'directory', + outputDirectory: TMP_DIR, + genUniqueDir: false, + }); + + // convert to the mdapi targetDir + // 1. maintain a list of “where this was originally” since we’ll need to handle MPD scenarios. + // Alteratively, the CS might have this. + // Alternatively, we could do a CS per packageDir + + // flip the preset in the sfdx-project.json + if (flags['dry-run']) { + this.log('TODO: dry-run output'); + } else { + projectJson.set('registryPresets', [...(projectJson.get('registryPresets') ?? []), flags.preset]); + await projectJson.write(); + } + + // delete the “original” files + if (flags['dry-run']) { + this.log(`would remove ${getComponentSetFiles(cs).join(', ')}`); + } else { + await Promise.all(getComponentSetFiles(cs).map((f) => rm(f))); + } + + // TODO: mdapi=>source convert the target dir back to the project + + if (!flags['preserve-temp-dir']) { + await rm(TMP_DIR, { recursive: true }); + } + + return { + presets: projectJson.get('registryPresets'), + }; + } +} + +/** get the LOCAL project json, throws if not present OR the preset already exists */ +const getValidatedProjectJson = (preset: string, project?: SfProject): SfProjectJson => { + const projectJson = project?.getSfProjectJson(false); + if (!projectJson) { + throw SfError.create({ name: 'ProjectJsonNotFound', message: 'sfdx-project.json not found' }); + } + if (projectJson.get('registryPresets')?.includes(preset)) { + throw SfError.create({ + name: 'PresetAlreadyExists', + message: `Preset ${preset} already exists in sfdx-project.json`, + }); + } + return projectJson; +}; + +const getComponentSetFiles = (cs: ComponentSet): string[] => + cs + .getSourceComponents() + .toArray() + .flatMap((c) => [c.xml, ...c.walkContent()]) + .filter(isString); diff --git a/test/commands/project/decompose.nut.ts b/test/commands/project/decompose.nut.ts new file mode 100644 index 00000000..0e83019b --- /dev/null +++ b/test/commands/project/decompose.nut.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 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 + */ +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; + +describe('project decompose NUTs', () => { + let session: TestSession; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); + }); + + after(async () => { + await session?.clean(); + }); + + it('should display provided name', () => { + const name = 'World'; + const command = `project decompose --name ${name}`; + const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; + expect(output).to.contain(name); + }); +}); diff --git a/yarn.lock b/yarn.lock index 56fd13d6..70e6d276 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7188,16 +7188,7 @@ srcset@^5.0.0: resolved "https://registry.yarnpkg.com/srcset/-/srcset-5.0.0.tgz#9df6c3961b5b44a02532ce6ae4544832609e2e3f" integrity sha512-SqEZaAEhe0A6ETEa9O1IhSPC7MdvehZtCnTR0AftXk3QhY2UNgb+NApFOUPZILXk/YTDfFxMTNJOBpzrJsEdIA== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7256,14 +7247,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7806,7 +7790,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7824,15 +7808,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 752ca32751f4c107aec67912b7b28288302f7c26 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 17 May 2024 12:58:30 -0500 Subject: [PATCH 02/20] feat: convert a project to use a new sourceBehaviorOption --- messages/project.decompose.md | 37 ++++-- src/commands/project/decompose.ts | 141 +++++++++------------- src/utils/decomposition.ts | 194 ++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 95 deletions(-) create mode 100644 src/utils/decomposition.ts diff --git a/messages/project.decompose.md b/messages/project.decompose.md index c3dd584f..805141fd 100644 --- a/messages/project.decompose.md +++ b/messages/project.decompose.md @@ -1,22 +1,22 @@ # summary -Enable a preset in sfdx-project.json and update your project source to use it. +Enable a sourceBehaviorOption in sfdx-project.json and update your project source to use it. # description -Makes local changes to your project based on the chosen preset. +Makes local changes to your project based on the chosen sourceBehaviorOption. -# flags.preset.summary +# flags.behavior.summary -Which preset to enable. +Which sourceBehaviorOption to enable. # examples - Switch the project to use decomposed custom labels - <%= config.bin %> <%= command.id %> --preset DecomposeCustomLabels --source-dir . + <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --source-dir . - Switch one packageDirectory to use decomposed custom labels - <%= config.bin %> <%= command.id %> --preset DecomposeCustomLabels --source-dir force-app + <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --source-dir force-app # flags.dry-run.summary @@ -26,6 +26,27 @@ Explain what the command would do but don't modify the project. Don't delete the metadata API format temp dir that this command creates. Useful for debugging. -# flags.source-dir.summary +# error.trackingNotSupported -Directory to modify the decomposition for. Can be an entire SfdxProject or any subfolder inside it. +The project has a target-org that uses source tracking. This operation will cause changes to the local project that can't be properly tracked. + +# error.trackingNotSupported.actions + +- Get any changes or data you need from the org +- Delete the org (`sf org delete scratch` or `sf org delete sandbox`) +- Run the command again +- Create a new org and deploy the modified source + +# error.packageDirectoryNeedsMainDefault + +The package directory %s does not have a main/default structure. +The command will move metadata into main/default which doesn't seem like what you'd want. + +# error.packageDirectoryNeedsMainDefault.actions + +- Update %s to have all its metadata inside main/default. +- Run the command again. + +# success.dryRun + +Files were created in %s outside your package directories for inspection. diff --git a/src/commands/project/decompose.ts b/src/commands/project/decompose.ts index 4a23c1dc..4116fade 100644 --- a/src/commands/project/decompose.ts +++ b/src/commands/project/decompose.ts @@ -5,33 +5,29 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { readdir, readFile, rm } from 'node:fs/promises'; -import { join } from 'node:path'; +import { rm, readFile, writeFile } from 'node:fs/promises'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; -import { Messages, SfError, SfProject, SfProjectJson } from '@salesforce/core'; +import { Messages } from '@salesforce/core'; import { - ComponentSet, - ComponentSetBuilder, - MetadataConverter, - MetadataRegistry, -} from '@salesforce/source-deploy-retrieve'; -import { isString } from '@salesforce/ts-types'; + getValidatedProjectJson, + TMP_DIR, + convertToMdapi, + DRY_RUN_DIR, + PRESETS_PROP, + PRESET_CHOICES, + getDecomposablePackageDirectories, + convertBackToSource, +} from '../../utils/decomposition.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'project.decompose'); export type ProjectDecomposeResult = { - presets: string[]; + [PRESETS_PROP]: string[]; + deletedFiles: string[]; + createdFiles: string[]; }; -// TODO: there must be a cleaner way to read this -const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( - 'file:', - '' -); -const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); -const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; - export default class ProjectDecompose extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -40,8 +36,8 @@ export default class ProjectDecompose extends SfCommand public static readonly requiresProject = true; public static readonly flags = { - preset: Flags.option({ - summary: messages.getMessage('flags.preset.summary'), + behavior: Flags.option({ + summary: messages.getMessage('flags.behavior.summary'), char: 'p', required: true, options: PRESET_CHOICES, @@ -52,85 +48,56 @@ export default class ProjectDecompose extends SfCommand 'preserve-temp-dir': Flags.boolean({ summary: messages.getMessage('flags.preserve-temp-dir.summary'), }), - 'source-dir': Flags.directory({ - summary: messages.getMessage('flags.source-dir.summary'), - char: 'd', - required: true, - multiple: true, - exists: true, - }), + 'target-org': Flags.optionalOrg(), }; public async run(): Promise { const { flags } = await this.parse(ProjectDecompose); - const projectJson = getValidatedProjectJson(flags.preset, this.project); - const typesFromPreset = Object.values( - (JSON.parse(await readFile(join(PRESET_DIR, `${flags.preset}.json`), 'utf-8')) as MetadataRegistry).types - ).map((t) => t.name); - - const cs = await ComponentSetBuilder.build({ - metadata: { - metadataEntries: typesFromPreset, - directoryPaths: this.project!.getPackageDirectories().map((pd) => pd.path), - }, - }); - - await new MetadataConverter().convert(cs, 'metadata', { - type: 'directory', - outputDirectory: TMP_DIR, - genUniqueDir: false, + if (await flags['target-org']?.supportsSourceTracking()) { + throw messages.createError('error.trackingNotSupported'); + } + const packageDirsWithDecomposable = await getDecomposablePackageDirectories(this.project!, flags.behavior); + const filesToDelete = await convertToMdapi(packageDirsWithDecomposable); + const projectJson = getValidatedProjectJson(flags.behavior, this.project!); + const backupPjsonContents = flags['dry-run'] ? await readFile(projectJson.getPath()) : ''; + + // flip the preset in the sfdx-project.json, even for dry-run, since the registry will need for conversions + projectJson.set(PRESETS_PROP, [...(projectJson.get(PRESETS_PROP) ?? []), flags.behavior]); + await projectJson.write(); + this.info(`sfdx-project.json ${PRESETS_PROP} is now [${projectJson.get(PRESETS_PROP).join(',')}]`); + + // delete the “original” files that no longer work because of project update + await Promise.all(flags['dry-run'] ? [] : filesToDelete.map((f) => rm(f))); + + const createdFiles = await convertBackToSource({ + packageDirsWithDecomposable, + projectDir: this.project!.getPath(), + dryRun: flags['dry-run'], }); - // convert to the mdapi targetDir - // 1. maintain a list of “where this was originally” since we’ll need to handle MPD scenarios. - // Alteratively, the CS might have this. - // Alternatively, we could do a CS per packageDir - - // flip the preset in the sfdx-project.json - if (flags['dry-run']) { - this.log('TODO: dry-run output'); - } else { - projectJson.set('registryPresets', [...(projectJson.get('registryPresets') ?? []), flags.preset]); - await projectJson.write(); + if (!flags['preserve-temp-dir']) { + await rm(TMP_DIR, { recursive: true }); } - // delete the “original” files + this.table( + filesToDelete.map((f) => ({ value: f })), + { value: { header: flags['dry-run'] ? 'Files that would have been deleted if not --dry-run' : 'Deleted Files' } } + ); + this.log(); + this.table( + createdFiles.map((f) => ({ value: f })), + { value: { header: 'Created Files' } } + ); if (flags['dry-run']) { - this.log(`would remove ${getComponentSetFiles(cs).join(', ')}`); - } else { - await Promise.all(getComponentSetFiles(cs).map((f) => rm(f))); - } - - // TODO: mdapi=>source convert the target dir back to the project - - if (!flags['preserve-temp-dir']) { - await rm(TMP_DIR, { recursive: true }); + // put it back how it was + await writeFile(projectJson.getPath(), backupPjsonContents); + this.logSuccess(messages.getMessage('success.dryRun', [DRY_RUN_DIR])); } return { - presets: projectJson.get('registryPresets'), + createdFiles, + deletedFiles: filesToDelete, + sourceBehaviorOptions: projectJson.get(PRESETS_PROP), }; } } - -/** get the LOCAL project json, throws if not present OR the preset already exists */ -const getValidatedProjectJson = (preset: string, project?: SfProject): SfProjectJson => { - const projectJson = project?.getSfProjectJson(false); - if (!projectJson) { - throw SfError.create({ name: 'ProjectJsonNotFound', message: 'sfdx-project.json not found' }); - } - if (projectJson.get('registryPresets')?.includes(preset)) { - throw SfError.create({ - name: 'PresetAlreadyExists', - message: `Preset ${preset} already exists in sfdx-project.json`, - }); - } - return projectJson; -}; - -const getComponentSetFiles = (cs: ComponentSet): string[] => - cs - .getSourceComponents() - .toArray() - .flatMap((c) => [c.xml, ...c.walkContent()]) - .filter(isString); diff --git a/src/utils/decomposition.ts b/src/utils/decomposition.ts new file mode 100644 index 00000000..7ca4840b --- /dev/null +++ b/src/utils/decomposition.ts @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2023, 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 + */ +import { existsSync, readdirSync } from 'node:fs'; +import { readFile, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; +import { SfError, SfProject, SfProjectJson, Messages } from '@salesforce/core'; +import { + ComponentSet, + ComponentSetBuilder, + ConvertResult, + MetadataConverter, + MetadataRegistry, + RegistryAccess, + SourceComponent, +} from '@salesforce/source-deploy-retrieve'; +import { isString } from '@salesforce/ts-types'; + +export type ComponentSetAndPackageDirPath = { packageDirPath: string; cs: ComponentSet }; + +// TODO: there must be a cleaner way to read this +const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( + 'file:', + '' +); +export const PRESETS_PROP = 'sourceBehaviorOptions'; +export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); +export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; + +/** returns packageDirectories and ComponentsSets where there is metadata of the type we'll decompose */ +export const getDecomposablePackageDirectories = async ( + project: SfProject, + preset: string +): Promise => + ( + await Promise.all( + project + .getPackageDirectories() + .map((pd) => pd.path) + .map(componentSetFromPackageDirectory(project.getPath())(await getTypesFromPreset(preset))) + ) + ) + .filter(componentSetIsNonEmpty) + // we do this after filtering componentSets to reduce false positives (ex: dir does not have main/default but also has nothing to decompose) + .map(validateMainDefault(project.getPath())); + +/** converts the composed metadata to mdapi format in a temp dir */ +export const convertToMdapi = async (packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]): Promise => + ( + await Promise.all( + packageDirsWithDecomposable.map(async (pd) => { + // convert to the mdapi targetDir + await new MetadataConverter().convert(pd.cs, 'metadata', { + type: 'directory', + outputDirectory: join(TMP_DIR, pd.packageDirPath), + genUniqueDir: false, + }); + + return getComponentSetFiles(pd.cs); + }) + ) + ).flat(); + +export const convertToSource = async ({ + packageDirsWithDecomposable, + projectDir, + dryRunDir, +}: { + packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]; + projectDir: string; + dryRunDir?: string; +}): Promise => { + // mdapi=>source convert the target dir back to the project + // it's a new converter because the project has changed and it should reload the project's registry. + const converter = new MetadataConverter(new RegistryAccess(undefined, projectDir)); + return Promise.all( + packageDirsWithDecomposable.map(async (pd) => + converter.convert( + // cs from the mdapi folder + await ComponentSetBuilder.build({ sourcepath: [join(TMP_DIR, pd.packageDirPath)], projectDir }), + 'source', + dryRunDir + ? // dryRun outputs to a dir outside the real packageDirs folder to avoid changing real stuff + { type: 'directory', genUniqueDir: false, outputDirectory: join(dryRunDir, pd.packageDirPath) } + : { + type: 'merge', + mergeWith: ( + await ComponentSetBuilder.build({ + sourcepath: [pd.packageDirPath], + projectDir, + }) + ).getSourceComponents(), + defaultDirectory: pd.packageDirPath, + } + ) + ) + ); +}; + +/** build a component set from the original project for each pkgDir */ +export const componentSetFromPackageDirectory = + (projectDir: string) => + (metadataEntries: string[]) => + async (packageDir: string): Promise => ({ + packageDirPath: packageDir, + cs: await ComponentSetBuilder.build({ + metadata: { + metadataEntries, + directoryPaths: [packageDir], + }, + projectDir, + }), + }); + +/** get the LOCAL project json, throws if not present OR the preset already exists */ +export const getValidatedProjectJson = (preset: string, project: SfProject): SfProjectJson => { + const projectJson = project.getSfProjectJson(false); + if (projectJson.get(PRESETS_PROP)?.includes(preset)) { + throw SfError.create({ + name: 'sourceBehaviorOptionAlreadyExists', + message: `sourceBehaviorOption ${preset} already exists in sfdx-project.json`, + }); + } + return projectJson; +}; + +const getSourceComponentFiles = (c: SourceComponent): string[] => + [ + c.xml, + ...(c.content ? readdirSync(c.content, { withFileTypes: true }).map((d) => join(d.path, d.name)) : []), + ].filter(isString); + +/** converts the temporary mdapi back to source, return a list of the created files */ +export const convertBackToSource = async ({ + packageDirsWithDecomposable, + projectDir, + dryRun, +}: { + packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]; + projectDir: string; + /** if provided, will output the results into a separate directory outside the project's packageDirectories */ + dryRun: boolean; +}): Promise => [ + ...new Set( + ( + await convertToSource({ + packageDirsWithDecomposable, + projectDir, + dryRunDir: dryRun ? DRY_RUN_DIR : undefined, + }) + ) + .flatMap((cr) => cr.converted ?? []) + // we can't use walkContent because there's a conditional inside it + .flatMap(getSourceComponentFiles) + .filter(isString) + ), +]; + +const getTypesFromPreset = async (preset: string): Promise => + Object.values( + (JSON.parse(await readFile(join(PRESET_DIR, `${preset}.json`), 'utf-8')) as MetadataRegistry).types + ).map((t) => t.name); + +/** convert will put things in /main/default. If the packageDirs aren't configured that way, we don't want to make a mess. + * See https://salesforce.quip.com/va5IAgXmTMWF for details on that issue */ +const validateMainDefault = + (projectDir: string) => + (i: ComponentSetAndPackageDirPath): ComponentSetAndPackageDirPath => { + if (!existsSync(join(projectDir, i.packageDirPath, 'main', 'default'))) { + throw loadMessages().createError( + 'error.packageDirectoryNeedsMainDefault', + [i.packageDirPath], + [i.packageDirPath] + ); + } + return i; + }; + +const getComponentSetFiles = (cs: ComponentSet): string[] => + cs + .getSourceComponents() + .toArray() + .flatMap((c) => [c.xml, ...c.walkContent()]) + .filter(isString); + +const loadMessages = (): Messages => { + Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); + return Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'project.decompose'); +}; +const componentSetIsNonEmpty = (i: ComponentSetAndPackageDirPath): boolean => i.cs.size > 0; +export const DRY_RUN_DIR = 'DRY-RUN-RESULTS'; From d0cc74f04a9c611f11161be1d4675e242c1a0c45 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 17 May 2024 13:00:21 -0500 Subject: [PATCH 03/20] chore: snapshot/schema --- command-snapshot.json | 8 +++++++ schemas/project-decompose.json | 31 ++++++++++++++++++++++++++++ schemas/project-delete-source.json | 10 ++++----- schemas/project-deploy-cancel.json | 8 +++---- schemas/project-deploy-quick.json | 8 +++---- schemas/project-deploy-report.json | 8 +++---- schemas/project-deploy-resume.json | 8 +++---- schemas/project-deploy-start.json | 8 +++---- schemas/project-deploy-validate.json | 8 +++---- schemas/project-retrieve-start.json | 8 +++---- 10 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 schemas/project-decompose.json diff --git a/command-snapshot.json b/command-snapshot.json index a10e03cc..e7b25fbf 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -36,6 +36,14 @@ ], "plugin": "@salesforce/plugin-deploy-retrieve" }, + { + "alias": [], + "command": "project:decompose", + "flagAliases": [], + "flagChars": ["o", "p"], + "flags": ["behavior", "dry-run", "flags-dir", "json", "preserve-temp-dir", "target-org"], + "plugin": "@salesforce/plugin-deploy-retrieve" + }, { "alias": ["force:source:delete"], "command": "project:delete:source", diff --git a/schemas/project-decompose.json b/schemas/project-decompose.json new file mode 100644 index 00000000..0e6645ec --- /dev/null +++ b/schemas/project-decompose.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/ProjectDecomposeResult", + "definitions": { + "ProjectDecomposeResult": { + "type": "object", + "properties": { + "sourceBehaviorOptions": { + "type": "array", + "items": { + "type": "string" + } + }, + "deletedFiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdFiles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["sourceBehaviorOptions", "deletedFiles", "createdFiles"], + "additionalProperties": false + } + } +} diff --git a/schemas/project-delete-source.json b/schemas/project-delete-source.json index daff80c4..e2fa16ca 100644 --- a/schemas/project-delete-source.json +++ b/schemas/project-delete-source.json @@ -444,6 +444,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -459,11 +460,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -492,11 +493,11 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "MetadataApiDeployStatus": { "type": "object", + "additionalProperties": false, "properties": { "id": { "type": "string" @@ -598,7 +599,6 @@ "status", "success" ], - "additionalProperties": false, "description": "Raw response returned from a checkDeployStatus call to the Metadata API" }, "CoverageResultsFileInfo": { diff --git a/schemas/project-deploy-cancel.json b/schemas/project-deploy-cancel.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-cancel.json +++ b/schemas/project-deploy-cancel.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-deploy-quick.json b/schemas/project-deploy-quick.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-quick.json +++ b/schemas/project-deploy-quick.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-deploy-report.json b/schemas/project-deploy-report.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-report.json +++ b/schemas/project-deploy-report.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-deploy-resume.json b/schemas/project-deploy-resume.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-resume.json +++ b/schemas/project-deploy-resume.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-deploy-start.json b/schemas/project-deploy-start.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-start.json +++ b/schemas/project-deploy-start.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-deploy-validate.json b/schemas/project-deploy-validate.json index 3355e767..a0756bda 100644 --- a/schemas/project-deploy-validate.json +++ b/schemas/project-deploy-validate.json @@ -142,6 +142,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -157,11 +158,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -190,8 +191,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "RequestStatus": { "type": "string", diff --git a/schemas/project-retrieve-start.json b/schemas/project-retrieve-start.json index 465b3a45..7176d623 100644 --- a/schemas/project-retrieve-start.json +++ b/schemas/project-retrieve-start.json @@ -72,6 +72,7 @@ }, "FileResponseSuccess": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -87,11 +88,11 @@ "enum": ["Created", "Changed", "Unchanged", "Deleted"] } }, - "required": ["fullName", "state", "type"], - "additionalProperties": false + "required": ["fullName", "state", "type"] }, "FileResponseFailure": { "type": "object", + "additionalProperties": false, "properties": { "fullName": { "type": "string" @@ -120,8 +121,7 @@ "enum": ["Warning", "Error"] } }, - "required": ["error", "fullName", "problemType", "state", "type"], - "additionalProperties": false + "required": ["error", "fullName", "problemType", "state", "type"] }, "FileProperties": { "type": "object", From af5efd62b47a751e2eec21a78d721bc035181a3b Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 17 May 2024 16:20:26 -0500 Subject: [PATCH 04/20] feat: error for behavior change with no matching types --- messages/project.decompose.md | 14 +++++++++++--- src/commands/project/decompose.ts | 2 +- src/utils/decomposition.ts | 9 +++++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/messages/project.decompose.md b/messages/project.decompose.md index 805141fd..aa4c9e97 100644 --- a/messages/project.decompose.md +++ b/messages/project.decompose.md @@ -15,12 +15,16 @@ Which sourceBehaviorOption to enable. - Switch the project to use decomposed custom labels <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --source-dir . -- Switch one packageDirectory to use decomposed custom labels - <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --source-dir force-app +- Without changing any existing files, see what the command would have produced. + <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --dry-run # flags.dry-run.summary -Explain what the command would do but don't modify the project. +Explain what the command would do. + +# flags.dry-run.description + +Doesn't modify existing files. Lists files that would be deleted, explains modifications to sfdx-project.json, and outputs the resulting modifications to a new folder for review. # flags.preserve-temp-dir.summary @@ -50,3 +54,7 @@ The command will move metadata into main/default which doesn't seem like what yo # success.dryRun Files were created in %s outside your package directories for inspection. + +# error.noTargetTypes + +The project contains no packageDirectories with metadata that matches the specified behavior %s. diff --git a/src/commands/project/decompose.ts b/src/commands/project/decompose.ts index 4116fade..95bf8f9a 100644 --- a/src/commands/project/decompose.ts +++ b/src/commands/project/decompose.ts @@ -56,9 +56,9 @@ export default class ProjectDecompose extends SfCommand if (await flags['target-org']?.supportsSourceTracking()) { throw messages.createError('error.trackingNotSupported'); } + const projectJson = getValidatedProjectJson(flags.behavior, this.project!); const packageDirsWithDecomposable = await getDecomposablePackageDirectories(this.project!, flags.behavior); const filesToDelete = await convertToMdapi(packageDirsWithDecomposable); - const projectJson = getValidatedProjectJson(flags.behavior, this.project!); const backupPjsonContents = flags['dry-run'] ? await readFile(projectJson.getPath()) : ''; // flip the preset in the sfdx-project.json, even for dry-run, since the registry will need for conversions diff --git a/src/utils/decomposition.ts b/src/utils/decomposition.ts index 7ca4840b..e1dbf4e5 100644 --- a/src/utils/decomposition.ts +++ b/src/utils/decomposition.ts @@ -34,8 +34,8 @@ export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterT export const getDecomposablePackageDirectories = async ( project: SfProject, preset: string -): Promise => - ( +): Promise => { + const output = ( await Promise.all( project .getPackageDirectories() @@ -46,6 +46,11 @@ export const getDecomposablePackageDirectories = async ( .filter(componentSetIsNonEmpty) // we do this after filtering componentSets to reduce false positives (ex: dir does not have main/default but also has nothing to decompose) .map(validateMainDefault(project.getPath())); + if (output.length === 0) { + loadMessages().createError('error.noTargetTypes', [preset]); + } + return output; +}; /** converts the composed metadata to mdapi format in a temp dir */ export const convertToMdapi = async (packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]): Promise => From cb156f045cadd10e1fe95b5e5d313a4b1eb7a605 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 20 May 2024 08:29:04 -0500 Subject: [PATCH 05/20] refactor: rename the command from design --- command-snapshot.json | 2 +- ...ecompose.md => convert.source-behavior.md} | 2 +- ... => project-convert-source__behavior.json} | 4 +- .../source-behavior.ts} | 22 +-- src/utils/decomposition.ts | 131 +++++++++--------- 5 files changed, 82 insertions(+), 79 deletions(-) rename messages/{project.decompose.md => convert.source-behavior.md} (98%) rename schemas/{project-decompose.json => project-convert-source__behavior.json} (88%) rename src/commands/project/{decompose.ts => convert/source-behavior.ts} (83%) diff --git a/command-snapshot.json b/command-snapshot.json index e7b25fbf..ac81543a 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -38,7 +38,7 @@ }, { "alias": [], - "command": "project:decompose", + "command": "project:convert:source-behavior", "flagAliases": [], "flagChars": ["o", "p"], "flags": ["behavior", "dry-run", "flags-dir", "json", "preserve-temp-dir", "target-org"], diff --git a/messages/project.decompose.md b/messages/convert.source-behavior.md similarity index 98% rename from messages/project.decompose.md rename to messages/convert.source-behavior.md index aa4c9e97..4d985a40 100644 --- a/messages/project.decompose.md +++ b/messages/convert.source-behavior.md @@ -13,7 +13,7 @@ Which sourceBehaviorOption to enable. # examples - Switch the project to use decomposed custom labels - <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --source-dir . + <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels - Without changing any existing files, see what the command would have produced. <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --dry-run diff --git a/schemas/project-decompose.json b/schemas/project-convert-source__behavior.json similarity index 88% rename from schemas/project-decompose.json rename to schemas/project-convert-source__behavior.json index 0e6645ec..43727542 100644 --- a/schemas/project-decompose.json +++ b/schemas/project-convert-source__behavior.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/ProjectDecomposeResult", + "$ref": "#/definitions/SourceBehaviorResult", "definitions": { - "ProjectDecomposeResult": { + "SourceBehaviorResult": { "type": "object", "properties": { "sourceBehaviorOptions": { diff --git a/src/commands/project/decompose.ts b/src/commands/project/convert/source-behavior.ts similarity index 83% rename from src/commands/project/decompose.ts rename to src/commands/project/convert/source-behavior.ts index 95bf8f9a..191c1810 100644 --- a/src/commands/project/decompose.ts +++ b/src/commands/project/convert/source-behavior.ts @@ -15,20 +15,20 @@ import { DRY_RUN_DIR, PRESETS_PROP, PRESET_CHOICES, - getDecomposablePackageDirectories, + getPackageDirectoriesForPreset, convertBackToSource, -} from '../../utils/decomposition.js'; +} from '../../../utils/decomposition.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'project.decompose'); +const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'convert.source-behavior'); -export type ProjectDecomposeResult = { +export type SourceBehaviorResult = { [PRESETS_PROP]: string[]; deletedFiles: string[]; createdFiles: string[]; }; -export default class ProjectDecompose extends SfCommand { +export default class ConvertSourceBehavior extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -51,15 +51,17 @@ export default class ProjectDecompose extends SfCommand 'target-org': Flags.optionalOrg(), }; - public async run(): Promise { - const { flags } = await this.parse(ProjectDecompose); + public async run(): Promise { + const { flags } = await this.parse(ConvertSourceBehavior); if (await flags['target-org']?.supportsSourceTracking()) { throw messages.createError('error.trackingNotSupported'); } const projectJson = getValidatedProjectJson(flags.behavior, this.project!); - const packageDirsWithDecomposable = await getDecomposablePackageDirectories(this.project!, flags.behavior); + const [backupPjsonContents, packageDirsWithDecomposable] = await Promise.all([ + flags['dry-run'] ? readFile(projectJson.getPath()) : '', + getPackageDirectoriesForPreset(this.project!, flags.behavior), + ]); const filesToDelete = await convertToMdapi(packageDirsWithDecomposable); - const backupPjsonContents = flags['dry-run'] ? await readFile(projectJson.getPath()) : ''; // flip the preset in the sfdx-project.json, even for dry-run, since the registry will need for conversions projectJson.set(PRESETS_PROP, [...(projectJson.get(PRESETS_PROP) ?? []), flags.behavior]); @@ -70,7 +72,7 @@ export default class ProjectDecompose extends SfCommand await Promise.all(flags['dry-run'] ? [] : filesToDelete.map((f) => rm(f))); const createdFiles = await convertBackToSource({ - packageDirsWithDecomposable, + packageDirsWithPreset: packageDirsWithDecomposable, projectDir: this.project!.getPath(), dryRun: flags['dry-run'], }); diff --git a/src/utils/decomposition.ts b/src/utils/decomposition.ts index e1dbf4e5..0ce3eec4 100644 --- a/src/utils/decomposition.ts +++ b/src/utils/decomposition.ts @@ -21,7 +21,7 @@ import { isString } from '@salesforce/ts-types'; export type ComponentSetAndPackageDirPath = { packageDirPath: string; cs: ComponentSet }; -// TODO: there must be a cleaner way to read this +// TODO: there could be a cleaner way to read this const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( 'file:', '' @@ -30,22 +30,23 @@ export const PRESETS_PROP = 'sourceBehaviorOptions'; export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; -/** returns packageDirectories and ComponentsSets where there is metadata of the type we'll decompose */ -export const getDecomposablePackageDirectories = async ( +/** returns packageDirectories and ComponentsSets where there is metadata of the type we'll change the behavior for */ +export const getPackageDirectoriesForPreset = async ( project: SfProject, preset: string ): Promise => { + const projectDir = project.getPath(); const output = ( await Promise.all( project .getPackageDirectories() .map((pd) => pd.path) - .map(componentSetFromPackageDirectory(project.getPath())(await getTypesFromPreset(preset))) + .map(componentSetFromPackageDirectory(projectDir)(await getTypesFromPreset(preset))) ) ) .filter(componentSetIsNonEmpty) // we do this after filtering componentSets to reduce false positives (ex: dir does not have main/default but also has nothing to decompose) - .map(validateMainDefault(project.getPath())); + .map(validateMainDefault(projectDir)); if (output.length === 0) { loadMessages().createError('error.noTargetTypes', [preset]); } @@ -69,57 +70,6 @@ export const convertToMdapi = async (packageDirsWithDecomposable: ComponentSetAn ) ).flat(); -export const convertToSource = async ({ - packageDirsWithDecomposable, - projectDir, - dryRunDir, -}: { - packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]; - projectDir: string; - dryRunDir?: string; -}): Promise => { - // mdapi=>source convert the target dir back to the project - // it's a new converter because the project has changed and it should reload the project's registry. - const converter = new MetadataConverter(new RegistryAccess(undefined, projectDir)); - return Promise.all( - packageDirsWithDecomposable.map(async (pd) => - converter.convert( - // cs from the mdapi folder - await ComponentSetBuilder.build({ sourcepath: [join(TMP_DIR, pd.packageDirPath)], projectDir }), - 'source', - dryRunDir - ? // dryRun outputs to a dir outside the real packageDirs folder to avoid changing real stuff - { type: 'directory', genUniqueDir: false, outputDirectory: join(dryRunDir, pd.packageDirPath) } - : { - type: 'merge', - mergeWith: ( - await ComponentSetBuilder.build({ - sourcepath: [pd.packageDirPath], - projectDir, - }) - ).getSourceComponents(), - defaultDirectory: pd.packageDirPath, - } - ) - ) - ); -}; - -/** build a component set from the original project for each pkgDir */ -export const componentSetFromPackageDirectory = - (projectDir: string) => - (metadataEntries: string[]) => - async (packageDir: string): Promise => ({ - packageDirPath: packageDir, - cs: await ComponentSetBuilder.build({ - metadata: { - metadataEntries, - directoryPaths: [packageDir], - }, - projectDir, - }), - }); - /** get the LOCAL project json, throws if not present OR the preset already exists */ export const getValidatedProjectJson = (preset: string, project: SfProject): SfProjectJson => { const projectJson = project.getSfProjectJson(false); @@ -132,19 +82,13 @@ export const getValidatedProjectJson = (preset: string, project: SfProject): SfP return projectJson; }; -const getSourceComponentFiles = (c: SourceComponent): string[] => - [ - c.xml, - ...(c.content ? readdirSync(c.content, { withFileTypes: true }).map((d) => join(d.path, d.name)) : []), - ].filter(isString); - /** converts the temporary mdapi back to source, return a list of the created files */ export const convertBackToSource = async ({ - packageDirsWithDecomposable, + packageDirsWithPreset, projectDir, dryRun, }: { - packageDirsWithDecomposable: ComponentSetAndPackageDirPath[]; + packageDirsWithPreset: ComponentSetAndPackageDirPath[]; projectDir: string; /** if provided, will output the results into a separate directory outside the project's packageDirectories */ dryRun: boolean; @@ -152,7 +96,7 @@ export const convertBackToSource = async ({ ...new Set( ( await convertToSource({ - packageDirsWithDecomposable, + packageDirsWithPreset, projectDir, dryRunDir: dryRun ? DRY_RUN_DIR : undefined, }) @@ -164,6 +108,63 @@ export const convertBackToSource = async ({ ), ]; +const getSourceComponentFiles = (c: SourceComponent): string[] => + [c.xml, ...(c.content ? fullPathsFromDir(c.content) : [])].filter(isString); + +const fullPathsFromDir = (dir: string): string[] => + readdirSync(dir, { withFileTypes: true }).map((d) => join(d.path, d.name)); + +/** build a component set from the original project for each pkgDir */ +const componentSetFromPackageDirectory = + (projectDir: string) => + (metadataEntries: string[]) => + async (packageDir: string): Promise => ({ + packageDirPath: packageDir, + cs: await ComponentSetBuilder.build({ + metadata: { + metadataEntries, + directoryPaths: [packageDir], + }, + projectDir, + }), + }); + +const convertToSource = async ({ + packageDirsWithPreset, + projectDir, + dryRunDir, +}: { + packageDirsWithPreset: ComponentSetAndPackageDirPath[]; + projectDir: string; + dryRunDir?: string; +}): Promise => { + // mdapi=>source convert the target dir back to the project + // it's a new converter because the project has changed and it should reload the project's registry. + const converter = new MetadataConverter(new RegistryAccess(undefined, projectDir)); + return Promise.all( + packageDirsWithPreset.map(async (pd) => + converter.convert( + // cs from the mdapi folder + await ComponentSetBuilder.build({ sourcepath: [join(TMP_DIR, pd.packageDirPath)], projectDir }), + 'source', + dryRunDir + ? // dryRun outputs to a dir outside the real packageDirs folder to avoid changing real stuff + { type: 'directory', genUniqueDir: false, outputDirectory: join(dryRunDir, pd.packageDirPath) } + : { + type: 'merge', + mergeWith: ( + await ComponentSetBuilder.build({ + sourcepath: [pd.packageDirPath], + projectDir, + }) + ).getSourceComponents(), + defaultDirectory: pd.packageDirPath, + } + ) + ) + ); +}; + const getTypesFromPreset = async (preset: string): Promise => Object.values( (JSON.parse(await readFile(join(PRESET_DIR, `${preset}.json`), 'utf-8')) as MetadataRegistry).types From a6aad0801fa595f08ddea4ed6af3f590e8dedcf9 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 20 May 2024 08:43:54 -0500 Subject: [PATCH 06/20] chore: ut a function --- src/utils/decomposition.ts | 2 +- test/utils/decomposition.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/utils/decomposition.test.ts diff --git a/src/utils/decomposition.ts b/src/utils/decomposition.ts index 0ce3eec4..3f846aea 100644 --- a/src/utils/decomposition.ts +++ b/src/utils/decomposition.ts @@ -165,7 +165,7 @@ const convertToSource = async ({ ); }; -const getTypesFromPreset = async (preset: string): Promise => +export const getTypesFromPreset = async (preset: string): Promise => Object.values( (JSON.parse(await readFile(join(PRESET_DIR, `${preset}.json`), 'utf-8')) as MetadataRegistry).types ).map((t) => t.name); diff --git a/test/utils/decomposition.test.ts b/test/utils/decomposition.test.ts new file mode 100644 index 00000000..0ad54a24 --- /dev/null +++ b/test/utils/decomposition.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, 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 + */ + +import { expect, assert } from 'chai'; +import { getTypesFromPreset } from '../../src/utils/decomposition.js'; + +describe('source behavior changes', () => { + describe('getTypesFromPreset', () => { + // TODO: update to a long-lived preset when the beta is removed + it('returns expected type for presets with sourceBehaviorOptions', async () => { + expect(await getTypesFromPreset('decomposeCustomLabelsBeta')).to.deep.equal(['CustomLabels']); + }); + it('throws ENOENT for non-existent presets', async () => { + try { + await getTypesFromPreset('nonExistentPreset'); + } catch (e) { + assert(e instanceof Error); + assert('code' in e); + expect(e.code).to.equal('ENOENT'); + } + }); + }); +}); From 046314293b275cd7ad503f9b9ece180cd65674d3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 08:41:44 -0500 Subject: [PATCH 07/20] test: add a nut, fix bugs that uncovers --- package.json | 1 + .../project/convert/source-behavior.ts | 2 +- .../{decomposition.ts => convertBehavior.ts} | 11 +- test/nuts/convert/decompose.nut.ts | 128 ++++++++++++++++++ test/utils/decomposition.test.ts | 2 +- yarn.lock | 5 + 6 files changed, 144 insertions(+), 5 deletions(-) rename src/utils/{decomposition.ts => convertBehavior.ts} (96%) create mode 100644 test/nuts/convert/decompose.nut.ts diff --git a/package.json b/package.json index 3c857844..325945d9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@salesforce/cli-plugins-testkit": "^5.3.2", "@salesforce/dev-scripts": "^9.1.1", "@salesforce/plugin-command-reference": "^3.0.83", + "@salesforce/schemas": "^1.9.0", "@salesforce/source-testkit": "^2.2.10", "@salesforce/ts-sinon": "^1.4.19", "cross-env": "^7.0.3", diff --git a/src/commands/project/convert/source-behavior.ts b/src/commands/project/convert/source-behavior.ts index 191c1810..54c4f4d3 100644 --- a/src/commands/project/convert/source-behavior.ts +++ b/src/commands/project/convert/source-behavior.ts @@ -17,7 +17,7 @@ import { PRESET_CHOICES, getPackageDirectoriesForPreset, convertBackToSource, -} from '../../../utils/decomposition.js'; +} from '../../../utils/convertBehavior.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'convert.source-behavior'); diff --git a/src/utils/decomposition.ts b/src/utils/convertBehavior.ts similarity index 96% rename from src/utils/decomposition.ts rename to src/utils/convertBehavior.ts index 3f846aea..c93ffd94 100644 --- a/src/utils/decomposition.ts +++ b/src/utils/convertBehavior.ts @@ -29,6 +29,7 @@ const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve' export const PRESETS_PROP = 'sourceBehaviorOptions'; export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; +export const DRY_RUN_DIR = 'DRY-RUN-RESULTS'; /** returns packageDirectories and ComponentsSets where there is metadata of the type we'll change the behavior for */ export const getPackageDirectoriesForPreset = async ( @@ -149,7 +150,12 @@ const convertToSource = async ({ 'source', dryRunDir ? // dryRun outputs to a dir outside the real packageDirs folder to avoid changing real stuff - { type: 'directory', genUniqueDir: false, outputDirectory: join(dryRunDir, pd.packageDirPath) } + { + type: 'directory', + outputDirectory: join(projectDir, dryRunDir), + packageName: pd.packageDirPath, + genUniqueDir: false, + } : { type: 'merge', mergeWith: ( @@ -194,7 +200,6 @@ const getComponentSetFiles = (cs: ComponentSet): string[] => const loadMessages = (): Messages => { Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); - return Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'project.decompose'); + return Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'convert.source-behavior'); }; const componentSetIsNonEmpty = (i: ComponentSetAndPackageDirPath): boolean => i.cs.size > 0; -export const DRY_RUN_DIR = 'DRY-RUN-RESULTS'; diff --git a/test/nuts/convert/decompose.nut.ts b/test/nuts/convert/decompose.nut.ts new file mode 100644 index 00000000..d1716738 --- /dev/null +++ b/test/nuts/convert/decompose.nut.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023, 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 + */ + +import path from 'node:path'; +import fs from 'node:fs'; +import { expect } from 'chai'; +import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { type ProjectJson } from '@salesforce/schemas'; +import { SourceBehaviorResult } from '../../../src/commands/project/convert/source-behavior.js'; +import { DRY_RUN_DIR } from '../../../src/utils/convertBehavior.js'; + +describe('source behavior changes', () => { + let session: TestSession; + before(async () => { + session = await TestSession.create({ + devhubAuthStrategy: 'NONE', + project: { + sourceDir: path.join(process.cwd(), 'test', 'nuts', 'customLabelProject'), + }, + }); + }); + + it('produces dry run output and makes no changes', async () => { + const originalProject = await getProject(session); + const originalFileList = await fs.promises.readdir(path.join(session.project.dir, 'force-app'), { + recursive: true, + }); + + const result = execCmd( + 'project convert source-behavior --behavior decomposeCustomLabelsBeta --dry-run --json', + { + ensureExitCode: 0, + } + ); + expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([ + 'force-app/main/default/labels/CustomLabels.labels-meta.xml', + ]); + expect(result.jsonOutput?.result.createdFiles).to.have.length(1); + result.jsonOutput?.result.createdFiles.map((f) => + expect(f.startsWith(path.join(DRY_RUN_DIR, 'force-app', 'main', 'default'))) + ); + expect(result.jsonOutput?.result.createdFiles); + // no change because dry run + expect(await getProject(session)).to.deep.equal(originalProject); + expect(await fs.promises.readdir(path.join(session.project.dir, 'force-app'), { recursive: true })).to.deep.equal( + originalFileList + ); + // dry run dir exists + expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR, 'force-app', 'main'))).to.be.true; + }); + + it('throws on a packageDir not using main/default', async () => { + const newDir = path.join(session.project.dir, 'other-dir'); + // create the new packageDir + await fs.promises.mkdir(path.join(newDir, 'labels'), { recursive: true }); + await fs.promises.writeFile(path.join(newDir, 'labels', 'CustomLabel.labels-meta.xml'), newLabelXml); + // add the new packageDir to the project + const originalProject = await getProject(session); + + await fs.promises.writeFile( + path.join(session.project.dir, 'sfdx-project.json'), + JSON.stringify( + { + ...originalProject, + packageDirectories: [...originalProject.packageDirectories, { path: 'other-dir' }], + } satisfies ProjectJson, + null, + 2 + ) + ); + + const result = execCmd('project convert source-behavior --behavior decomposeCustomLabelsBeta --json', { + ensureExitCode: 1, + }); + expect(result.jsonOutput?.name).to.equal('PackageDirectoryNeedsMainDefaultError'); + // put stuff back the way it was + await fs.promises.rm(newDir, { recursive: true }); + await fs.promises.writeFile( + path.join(session.project.dir, 'sfdx-project.json'), + JSON.stringify(originalProject, null, 2) + ); + }); + + it('produces actual output and makes expected changes', async () => { + const result = execCmd( + 'project convert source-behavior --behavior decomposeCustomLabelsBeta --json', + { + ensureExitCode: 0, + } + ); + expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([ + 'force-app/main/default/labels/CustomLabels.labels-meta.xml', + ]); + expect(result.jsonOutput?.result.createdFiles).to.have.length(1); + expect(result.jsonOutput?.result.createdFiles).to.deep.equal([]); + // no change because dry run + expect(await getProject(session)).to.deep.equal(originalProject); + expect(await fs.promises.readdir(path.join(session.project.dir, 'force-app'), { recursive: true })).to.deep.equal( + originalFileList + ); + // dry run dir exists + expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR, 'force-app', 'main'))).to.be.true; + }); + + it("throws on repeated preset that's already done"); + after(async () => { + await session?.clean(); + }); +}); + +const getProject = async (session: TestSession): Promise => + JSON.parse(await fs.promises.readFile(path.join(session.project.dir, 'sfdx-project.json'), 'utf-8')) as ProjectJson; + +const newLabelXml = ` + + + More + en_US + true + DeleteMe + More + + +`; diff --git a/test/utils/decomposition.test.ts b/test/utils/decomposition.test.ts index 0ad54a24..b20b2f38 100644 --- a/test/utils/decomposition.test.ts +++ b/test/utils/decomposition.test.ts @@ -6,7 +6,7 @@ */ import { expect, assert } from 'chai'; -import { getTypesFromPreset } from '../../src/utils/decomposition.js'; +import { getTypesFromPreset } from '../../src/utils/convertBehavior.js'; describe('source behavior changes', () => { describe('getTypesFromPreset', () => { diff --git a/yarn.lock b/yarn.lock index 70e6d276..ad2deae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1558,6 +1558,11 @@ resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.7.0.tgz#b7e0af3ee414ae7160bce351c0184d77ccb98fe3" integrity sha512-Z0PiCEV55khm0PG+DsnRYCjaDmacNe3HDmsoSm/CSyYvJJm+D5vvkHKN9/PKD/gaRe8XAU836yfamIYFblLINw== +"@salesforce/schemas@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.9.0.tgz#ba477a112653a20b4edcf989c61c57bdff9aa3ca" + integrity sha512-LiN37zG5ODT6z70sL1fxF7BQwtCX9JOWofSU8iliSNIM+WDEeinnoFtVqPInRSNt8I0RiJxIKCrqstsmQRBNvA== + "@salesforce/sf-plugins-core@^9.0.5", "@salesforce/sf-plugins-core@^9.0.7": version "9.0.7" resolved "https://registry.yarnpkg.com/@salesforce/sf-plugins-core/-/sf-plugins-core-9.0.7.tgz#77ffc67df994e0cec205827462811f521e3086ba" From 3085af4122fd00cfa9a8cc54d1e9d55f4913cbe9 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 09:57:10 -0500 Subject: [PATCH 08/20] chore: bump sdr for renamed prop --- package.json | 4 ++-- yarn.lock | 43 +++++++++++++++++++------------------------ 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 325945d9..8d77a8ed 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "@salesforce/kit": "^3.1.1", "@salesforce/plugin-info": "^3.2.7", "@salesforce/sf-plugins-core": "^9.0.7", - "@salesforce/source-deploy-retrieve": "^11.4.3", - "@salesforce/source-tracking": "^6.0.4", + "@salesforce/source-deploy-retrieve": "^11.6.0", + "@salesforce/source-tracking": "^6.2.3", "@salesforce/ts-types": "^2.0.9", "chalk": "^5.3.0" }, diff --git a/yarn.lock b/yarn.lock index ad2deae4..653f115a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1335,7 +1335,7 @@ wordwrap "^1.0.0" wrap-ansi "^7.0.0" -"@oclif/core@^3.26.0", "@oclif/core@^3.26.2", "@oclif/core@^3.26.4", "@oclif/core@^3.26.5", "@oclif/core@^3.26.6": +"@oclif/core@^3.26.0", "@oclif/core@^3.26.2", "@oclif/core@^3.26.5", "@oclif/core@^3.26.6": version "3.26.6" resolved "https://registry.yarnpkg.com/@oclif/core/-/core-3.26.6.tgz#f371868cfa0fe150a6547e6af98b359065d2f971" integrity sha512-+FiTw1IPuJTF9tSAlTsY8bGK4sgthehjz7c2SvYdgQncTkxI2xvUch/8QpjNYGLEmUneNygvYMRBax2KJcLccA== @@ -1449,14 +1449,14 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.0" -"@salesforce/core@^7.2.0", "@salesforce/core@^7.3.0", "@salesforce/core@^7.3.1", "@salesforce/core@^7.3.3", "@salesforce/core@^7.3.4", "@salesforce/core@^7.3.5": - version "7.3.5" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-7.3.5.tgz#0dccb4d8ddd36cde449d67a482dfb52e63de1c9f" - integrity sha512-9hkK4EyV1Z7T1mDyb/Rj1dO0Owp3f2PNGXSyQhCG2nozSCxAQlPeFFqn2L3d7kJJxdhlr58P4QXiFCoJVVvbLQ== +"@salesforce/core@^7.2.0", "@salesforce/core@^7.3.1", "@salesforce/core@^7.3.3", "@salesforce/core@^7.3.4", "@salesforce/core@^7.3.5", "@salesforce/core@^7.3.8": + version "7.3.9" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-7.3.9.tgz#8abe2b3e2393989d11e92b7a6b96043fc9d5b9c8" + integrity sha512-eJqDiA5b7wU50Ee/xjmGzSnHrNVJ8S77B7enfX30gm7gxU3i3M3QeBdiV6XAOPLSIL96DseofP6Tv6c+rljlKA== dependencies: "@jsforce/jsforce-node" "^3.2.0" "@salesforce/kit" "^3.1.1" - "@salesforce/schemas" "^1.7.0" + "@salesforce/schemas" "^1.9.0" "@salesforce/ts-types" "^2.0.9" ajv "^8.13.0" change-case "^4.1.2" @@ -1469,7 +1469,7 @@ pino-abstract-transport "^1.1.0" pino-pretty "^10.3.1" proper-lockfile "^4.1.2" - semver "^7.6.0" + semver "^7.6.2" ts-retry-promise "^0.7.1" "@salesforce/dev-config@^4.1.0": @@ -1553,11 +1553,6 @@ resolved "https://registry.yarnpkg.com/@salesforce/prettier-config/-/prettier-config-0.0.3.tgz#ba648d4886bb38adabe073dbea0b3a91b3753bb0" integrity sha512-hYOhoPTCSYMDYn+U1rlEk16PoBeAJPkrdg4/UtAzupM1mRRJOwEPMG1d7U8DxJFKuXW3DMEYWr2MwAIBDaHmFg== -"@salesforce/schemas@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.7.0.tgz#b7e0af3ee414ae7160bce351c0184d77ccb98fe3" - integrity sha512-Z0PiCEV55khm0PG+DsnRYCjaDmacNe3HDmsoSm/CSyYvJJm+D5vvkHKN9/PKD/gaRe8XAU836yfamIYFblLINw== - "@salesforce/schemas@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@salesforce/schemas/-/schemas-1.9.0.tgz#ba477a112653a20b4edcf989c61c57bdff9aa3ca" @@ -1576,10 +1571,10 @@ "@salesforce/ts-types" "^2.0.9" chalk "^5.3.0" -"@salesforce/source-deploy-retrieve@^11.0.1", "@salesforce/source-deploy-retrieve@^11.3.0", "@salesforce/source-deploy-retrieve@^11.4.3": - version "11.4.3" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-11.4.3.tgz#57ff91f5f0021804b268002362a695489f34ac5a" - integrity sha512-/WFSqf+yTO3kNM41r/duVEHGH2Eq18lavX/o/yA0hJEmxAYBV7Olv6JIMnbly8hZQo7LoX/nE0LnNK3JYphZQQ== +"@salesforce/source-deploy-retrieve@^11.3.0", "@salesforce/source-deploy-retrieve@^11.4.4", "@salesforce/source-deploy-retrieve@^11.6.0": + version "11.6.0" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-11.6.0.tgz#e17ca375996deb238ac4bc199d0c71537f1719cb" + integrity sha512-KZnYTMfjnKMczKsTRDm7XgFAwYSeK8o8+OtMSUyZlcosNBb6ece8R+X/8WoqWj/Mbc8MqNPnQz90Ch8aGZZB0g== dependencies: "@salesforce/core" "^7.3.5" "@salesforce/kit" "^3.1.1" @@ -1610,15 +1605,15 @@ shelljs "^0.8.4" sinon "^10.0.0" -"@salesforce/source-tracking@^6.0.4": - version "6.0.4" - resolved "https://registry.yarnpkg.com/@salesforce/source-tracking/-/source-tracking-6.0.4.tgz#45b8cfa6fc1d3dee0f265de975340c13629f1176" - integrity sha512-4+QFC6hm2MHCUsQdgSNydSoyi9zJaFl1S6rwJl/HTGeISs3g8w3tI+SeskmpZCoXRVnNJxCHwHT3kAPhZbuFlg== +"@salesforce/source-tracking@^6.2.3": + version "6.2.3" + resolved "https://registry.yarnpkg.com/@salesforce/source-tracking/-/source-tracking-6.2.3.tgz#5fe4e8d3704b74fdda1f3893412c8e2a0694f3a5" + integrity sha512-XFoRjhpVAiNyMmXqVIAte3yPaRBu5ZDlU0YzYAGfS0TJ82c+Iy2SYRDOI8pWJ29gIj8PsrQm5c1xBsVFI5eiew== dependencies: - "@oclif/core" "^3.26.4" - "@salesforce/core" "^7.3.0" + "@oclif/core" "^3.26.6" + "@salesforce/core" "^7.3.8" "@salesforce/kit" "^3.1.1" - "@salesforce/source-deploy-retrieve" "^11.0.1" + "@salesforce/source-deploy-retrieve" "^11.4.4" "@salesforce/ts-types" "^2.0.9" fast-xml-parser "^4.3.6" graceful-fs "^4.2.11" @@ -6844,7 +6839,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0: +semver@^7.0.0, semver@^7.3.4, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== From 0d6b613940d0b0af440988e201f4c5f9bfc0e9e6 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 10:18:17 -0500 Subject: [PATCH 09/20] feat: normalize paths for consistency --- src/utils/convertBehavior.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utils/convertBehavior.ts b/src/utils/convertBehavior.ts index c93ffd94..863580dd 100644 --- a/src/utils/convertBehavior.ts +++ b/src/utils/convertBehavior.ts @@ -6,7 +6,7 @@ */ import { existsSync, readdirSync } from 'node:fs'; import { readFile, readdir } from 'node:fs/promises'; -import { join } from 'node:path'; +import { join, resolve } from 'node:path'; import { SfError, SfProject, SfProjectJson, Messages } from '@salesforce/core'; import { ComponentSet, @@ -69,7 +69,9 @@ export const convertToMdapi = async (packageDirsWithDecomposable: ComponentSetAn return getComponentSetFiles(pd.cs); }) ) - ).flat(); + ) + .flat() + .map((f) => resolve(f)); /** get the LOCAL project json, throws if not present OR the preset already exists */ export const getValidatedProjectJson = (preset: string, project: SfProject): SfProjectJson => { @@ -145,7 +147,7 @@ const convertToSource = async ({ return Promise.all( packageDirsWithPreset.map(async (pd) => converter.convert( - // cs from the mdapi folder + // componentSet based on each mdapi folder await ComponentSetBuilder.build({ sourcepath: [join(TMP_DIR, pd.packageDirPath)], projectDir }), 'source', dryRunDir @@ -164,7 +166,7 @@ const convertToSource = async ({ projectDir, }) ).getSourceComponents(), - defaultDirectory: pd.packageDirPath, + defaultDirectory: join(projectDir, pd.packageDirPath), } ) ) From eeea4df6e72c2b51548db2c102c32c568f5a31c1 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 10:49:28 -0500 Subject: [PATCH 10/20] test: more nut for preset conversion --- test/nuts/convert/decompose.nut.ts | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/test/nuts/convert/decompose.nut.ts b/test/nuts/convert/decompose.nut.ts index d1716738..d17e8d6c 100644 --- a/test/nuts/convert/decompose.nut.ts +++ b/test/nuts/convert/decompose.nut.ts @@ -11,7 +11,7 @@ import { expect } from 'chai'; import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; import { type ProjectJson } from '@salesforce/schemas'; import { SourceBehaviorResult } from '../../../src/commands/project/convert/source-behavior.js'; -import { DRY_RUN_DIR } from '../../../src/utils/convertBehavior.js'; +import { DRY_RUN_DIR, PRESETS_PROP } from '../../../src/utils/convertBehavior.js'; describe('source behavior changes', () => { let session: TestSession; @@ -37,11 +37,11 @@ describe('source behavior changes', () => { } ); expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([ - 'force-app/main/default/labels/CustomLabels.labels-meta.xml', + path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml'), ]); - expect(result.jsonOutput?.result.createdFiles).to.have.length(1); + expect(result.jsonOutput?.result.createdFiles).to.have.length(4); result.jsonOutput?.result.createdFiles.map((f) => - expect(f.startsWith(path.join(DRY_RUN_DIR, 'force-app', 'main', 'default'))) + expect(f.startsWith(path.join(session.project.dir, DRY_RUN_DIR, 'force-app', 'main', 'default'))) ); expect(result.jsonOutput?.result.createdFiles); // no change because dry run @@ -51,6 +51,7 @@ describe('source behavior changes', () => { ); // dry run dir exists expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR, 'force-app', 'main'))).to.be.true; + await fs.promises.rm(path.join(session.project.dir, DRY_RUN_DIR), { recursive: true }); }); it('throws on a packageDir not using main/default', async () => { @@ -93,20 +94,26 @@ describe('source behavior changes', () => { } ); expect(result.jsonOutput?.result.deletedFiles).to.deep.equal([ - 'force-app/main/default/labels/CustomLabels.labels-meta.xml', + path.join(session.project.dir, 'force-app', 'main', 'default', 'labels', 'CustomLabels.labels-meta.xml'), ]); - expect(result.jsonOutput?.result.createdFiles).to.have.length(1); - expect(result.jsonOutput?.result.createdFiles).to.deep.equal([]); - // no change because dry run - expect(await getProject(session)).to.deep.equal(originalProject); - expect(await fs.promises.readdir(path.join(session.project.dir, 'force-app'), { recursive: true })).to.deep.equal( - originalFileList + expect(result.jsonOutput?.result.createdFiles).to.have.length(4); + // it modified the project json + expect((await getProject(session))[PRESETS_PROP]).to.deep.equal(['decomposeCustomLabelsBeta']); + + // no dry run dir + expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR))).to.be.false; + }); + + it("throws on repeated preset that's already done", () => { + const err = execCmd( + 'project convert source-behavior --behavior decomposeCustomLabelsBeta --json', + { + ensureExitCode: 1, + } ); - // dry run dir exists - expect(fs.existsSync(path.join(session.project.dir, DRY_RUN_DIR, 'force-app', 'main'))).to.be.true; + expect(err.jsonOutput?.name).to.equal('sourceBehaviorOptionAlreadyExists'); }); - it("throws on repeated preset that's already done"); after(async () => { await session?.clean(); }); From 68a6548e4c4e1ae0a0e8658d21a8aa0a0e38242a Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 11:14:39 -0500 Subject: [PATCH 11/20] test: remove redundant nut --- test/commands/project/decompose.nut.ts | 27 -------------------------- 1 file changed, 27 deletions(-) delete mode 100644 test/commands/project/decompose.nut.ts diff --git a/test/commands/project/decompose.nut.ts b/test/commands/project/decompose.nut.ts deleted file mode 100644 index 0e83019b..00000000 --- a/test/commands/project/decompose.nut.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2023, 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 - */ -import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; -import { expect } from 'chai'; - -describe('project decompose NUTs', () => { - let session: TestSession; - - before(async () => { - session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); - }); - - after(async () => { - await session?.clean(); - }); - - it('should display provided name', () => { - const name = 'World'; - const command = `project decompose --name ${name}`; - const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; - expect(output).to.contain(name); - }); -}); From bd5e7d84743fcc3ab7c33d6758995cd7be6b9683 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 11:37:57 -0500 Subject: [PATCH 12/20] test: win ut only --- .github/workflows/test.yml | 66 ++++++++++++++++++------------------ src/utils/convertBehavior.ts | 12 ++++--- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9510c0f5..cd538d0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,39 +5,39 @@ on: workflow_dispatch: jobs: - yarn-lockfile-check: - uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main + # yarn-lockfile-check: + # uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main # 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 - linux-unit-tests: - needs: yarn-lockfile-check - uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main + # linux-unit-tests: + # needs: yarn-lockfile-check + # uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main windows-unit-tests: - needs: linux-unit-tests + # needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main - nuts: - needs: linux-unit-tests - uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main - secrets: inherit - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - command: - - 'yarn test:nuts:deb' - - 'yarn test:nuts:deploy' - - 'yarn test:nuts:deploy:metadata:manifest' - - 'yarn test:nuts:deploy:metadata:metadata-dir' - - 'yarn test:nuts:deploy:metadata:metadata' - - 'yarn test:nuts:deploy:metadata:source-dir' - - 'yarn test:nuts:deploy:metadata:test-level' - - 'yarn test:nuts:static' - - 'yarn test:nuts:retrieve' - - 'yarn test:nuts:delete' - - 'yarn test:nuts:destructive' - - 'yarn test:nuts:convert' - - 'yarn test:nuts:manifest' - - 'yarn test:nuts:specialTypes' - - 'yarn test:nuts:tracking' - fail-fast: false - with: - os: ${{ matrix.os }} - command: ${{ matrix.command }} + # nuts: + # # needs: linux-unit-tests + # uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main + # secrets: inherit + # strategy: + # matrix: + # os: [ubuntu-latest, windows-latest] + # command: + # - 'yarn test:nuts:deb' + # - 'yarn test:nuts:deploy' + # - 'yarn test:nuts:deploy:metadata:manifest' + # - 'yarn test:nuts:deploy:metadata:metadata-dir' + # - 'yarn test:nuts:deploy:metadata:metadata' + # - 'yarn test:nuts:deploy:metadata:source-dir' + # - 'yarn test:nuts:deploy:metadata:test-level' + # - 'yarn test:nuts:static' + # - 'yarn test:nuts:retrieve' + # - 'yarn test:nuts:delete' + # - 'yarn test:nuts:destructive' + # - 'yarn test:nuts:convert' + # - 'yarn test:nuts:manifest' + # - 'yarn test:nuts:specialTypes' + # - 'yarn test:nuts:tracking' + # fail-fast: false + # with: + # os: ${{ matrix.os }} + # command: ${{ matrix.command }} diff --git a/src/utils/convertBehavior.ts b/src/utils/convertBehavior.ts index 863580dd..fe50da4a 100644 --- a/src/utils/convertBehavior.ts +++ b/src/utils/convertBehavior.ts @@ -5,6 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { existsSync, readdirSync } from 'node:fs'; +import { platform } from 'node:os'; import { readFile, readdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; import { SfError, SfProject, SfProjectJson, Messages } from '@salesforce/core'; @@ -22,12 +23,13 @@ import { isString } from '@salesforce/ts-types'; export type ComponentSetAndPackageDirPath = { packageDirPath: string; cs: ComponentSet }; // TODO: there could be a cleaner way to read this -const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( - 'file:', - '' -); +const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets'); +// .replace( 'file:', '' ); + export const PRESETS_PROP = 'sourceBehaviorOptions'; -export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); +export const PRESET_CHOICES = ( + await readdir(platform() === 'win32' ? PRESET_DIR : PRESET_DIR.replace('file:', '')) +).map((f) => f.replace('.json', '')); export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; export const DRY_RUN_DIR = 'DRY-RUN-RESULTS'; From da93e05a021077f52606fe8902b575721e6c9e0f Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 11:42:16 -0500 Subject: [PATCH 13/20] refactor: calc preset dir once --- src/utils/convertBehavior.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/convertBehavior.ts b/src/utils/convertBehavior.ts index fe50da4a..3a3ae088 100644 --- a/src/utils/convertBehavior.ts +++ b/src/utils/convertBehavior.ts @@ -23,13 +23,14 @@ import { isString } from '@salesforce/ts-types'; export type ComponentSetAndPackageDirPath = { packageDirPath: string; cs: ComponentSet }; // TODO: there could be a cleaner way to read this -const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets'); +const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( + 'file:', + platform() === 'win32' ? 'file:' : '' +); // .replace( 'file:', '' ); export const PRESETS_PROP = 'sourceBehaviorOptions'; -export const PRESET_CHOICES = ( - await readdir(platform() === 'win32' ? PRESET_DIR : PRESET_DIR.replace('file:', '')) -).map((f) => f.replace('.json', '')); +export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; export const DRY_RUN_DIR = 'DRY-RUN-RESULTS'; From e42b798010c160e20fe7ca0c244da026ec8d3a8a Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 11:52:57 -0500 Subject: [PATCH 14/20] test: windows ut again --- src/utils/convertBehavior.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/utils/convertBehavior.ts b/src/utils/convertBehavior.ts index 3a3ae088..e39da1e2 100644 --- a/src/utils/convertBehavior.ts +++ b/src/utils/convertBehavior.ts @@ -5,9 +5,10 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { existsSync, readdirSync } from 'node:fs'; -import { platform } from 'node:os'; +// import { platform } from 'node:os'; import { readFile, readdir } from 'node:fs/promises'; import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { SfError, SfProject, SfProjectJson, Messages } from '@salesforce/core'; import { ComponentSet, @@ -23,12 +24,9 @@ import { isString } from '@salesforce/ts-types'; export type ComponentSetAndPackageDirPath = { packageDirPath: string; cs: ComponentSet }; // TODO: there could be a cleaner way to read this -const PRESET_DIR = join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets').replace( - 'file:', - platform() === 'win32' ? 'file:' : '' +const PRESET_DIR = fileURLToPath( + join(import.meta.resolve('@salesforce/source-deploy-retrieve'), '..', 'registry', 'presets') ); -// .replace( 'file:', '' ); - export const PRESETS_PROP = 'sourceBehaviorOptions'; export const PRESET_CHOICES = (await readdir(PRESET_DIR)).map((f) => f.replace('.json', '')); export const TMP_DIR = process.env.SF_MDAPI_TEMP_DIR ?? 'decompositionConverterTempDir'; From eaf7a2bc7d38fc6e58f39ff821da2d4d97960baa Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 21 May 2024 11:57:56 -0500 Subject: [PATCH 15/20] test: restore full test suite --- .github/workflows/test.yml | 64 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd538d0a..e1f49a47 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,39 +5,39 @@ on: workflow_dispatch: jobs: - # yarn-lockfile-check: - # uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main + yarn-lockfile-check: + uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main # 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 - # linux-unit-tests: - # needs: yarn-lockfile-check - # uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main + linux-unit-tests: + needs: yarn-lockfile-check + uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main windows-unit-tests: # needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main - # nuts: - # # needs: linux-unit-tests - # uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main - # secrets: inherit - # strategy: - # matrix: - # os: [ubuntu-latest, windows-latest] - # command: - # - 'yarn test:nuts:deb' - # - 'yarn test:nuts:deploy' - # - 'yarn test:nuts:deploy:metadata:manifest' - # - 'yarn test:nuts:deploy:metadata:metadata-dir' - # - 'yarn test:nuts:deploy:metadata:metadata' - # - 'yarn test:nuts:deploy:metadata:source-dir' - # - 'yarn test:nuts:deploy:metadata:test-level' - # - 'yarn test:nuts:static' - # - 'yarn test:nuts:retrieve' - # - 'yarn test:nuts:delete' - # - 'yarn test:nuts:destructive' - # - 'yarn test:nuts:convert' - # - 'yarn test:nuts:manifest' - # - 'yarn test:nuts:specialTypes' - # - 'yarn test:nuts:tracking' - # fail-fast: false - # with: - # os: ${{ matrix.os }} - # command: ${{ matrix.command }} + nuts: + # needs: linux-unit-tests + uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main + secrets: inherit + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + command: + - 'yarn test:nuts:deb' + - 'yarn test:nuts:deploy' + - 'yarn test:nuts:deploy:metadata:manifest' + - 'yarn test:nuts:deploy:metadata:metadata-dir' + - 'yarn test:nuts:deploy:metadata:metadata' + - 'yarn test:nuts:deploy:metadata:source-dir' + - 'yarn test:nuts:deploy:metadata:test-level' + - 'yarn test:nuts:static' + - 'yarn test:nuts:retrieve' + - 'yarn test:nuts:delete' + - 'yarn test:nuts:destructive' + - 'yarn test:nuts:convert' + - 'yarn test:nuts:manifest' + - 'yarn test:nuts:specialTypes' + - 'yarn test:nuts:tracking' + fail-fast: false + with: + os: ${{ matrix.os }} + command: ${{ matrix.command }} From cb32aa1301b4fb40c6acaaf8b5c1d2ccf673cc28 Mon Sep 17 00:00:00 2001 From: Juliet Shackell <63259011+jshackell-sfdc@users.noreply.github.com> Date: Fri, 24 May 2024 05:52:59 -0700 Subject: [PATCH 16/20] fix: edit the messages for "project convert source-behavior" (#1018) --- messages/convert.source-behavior.md | 50 +++++++++++++++++------------ package.json | 2 +- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/messages/convert.source-behavior.md b/messages/convert.source-behavior.md index 4d985a40..d28657ac 100644 --- a/messages/convert.source-behavior.md +++ b/messages/convert.source-behavior.md @@ -1,60 +1,70 @@ # summary -Enable a sourceBehaviorOption in sfdx-project.json and update your project source to use it. +Enable a behavior of your project source files, and then update your Salesforce DX project to implement the behavior. # description -Makes local changes to your project based on the chosen sourceBehaviorOption. +Specifically, this command updates the "sourceBehaviorOption" option in the "sfdx-project.json" file and then converts the associated local source files in your project as needed. + +For example, run this command with the "--behavior decomposePermissionSet" flag to start decomposing permission sets when you deploy or retrieve them. Decomposing means breaking up the monolithic metadata API format XML file that corresponds to a metadata component into smaller XML files and directories based on its subtypes. Permission sets are not decomposed by default; you must opt-in to start decomposing them by using this command. When the command finishes, your "sfdx-project.json" file is updated to always decompose permission sets, and the existing permission set files in your local package directories are converted into the new decomposed format. You run this command only once for a given behavior change. + +For more information about the possible values for the --behavior flag, see the "sourceBehaviorOptions" section in the https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm topic. # flags.behavior.summary -Which sourceBehaviorOption to enable. +Behavior to enable; the values correspond to the possible values of the "sourceBehaviorOption" option in the "sfdx-project.json" file. # examples -- Switch the project to use decomposed custom labels - <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels +- Update your Salesforce DX project to decompose custom labels: + + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels + +- Display what the command would do, but don't change any existing files: + + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels --dry-run + +- Keep the temporary directory that contains the interim metadata API formatted files: -- Without changing any existing files, see what the command would have produced. - <%= config.bin %> <%= command.id %> --behavior DecomposeCustomLabels --dry-run + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels --dry-run --preserve-temp-dir # flags.dry-run.summary -Explain what the command would do. +Display what the command would do, but don't make any actual changes. # flags.dry-run.description -Doesn't modify existing files. Lists files that would be deleted, explains modifications to sfdx-project.json, and outputs the resulting modifications to a new folder for review. +Doesn't modify the existing files in your project, including the "sfdx-project.json" file. Instead, the command lists the files that would be deleted, explains the modifications to the "sfdx-project.json" file, and outputs the resulting modifications to a new directory for review. # flags.preserve-temp-dir.summary -Don't delete the metadata API format temp dir that this command creates. Useful for debugging. +Don't delete the metadata API format temporary directory that this command creates. Useful for debugging. # error.trackingNotSupported -The project has a target-org that uses source tracking. This operation will cause changes to the local project that can't be properly tracked. +Your project has a default org (target-org) that uses source tracking. This operation will cause changes to the local project source files that can't be properly tracked. # error.trackingNotSupported.actions -- Get any changes or data you need from the org -- Delete the org (`sf org delete scratch` or `sf org delete sandbox`) -- Run the command again -- Create a new org and deploy the modified source +- Retrieve any changes or data you need from the org that you haven't already retrieved. +- Delete the org ("sf org delete scratch" or "sf org delete sandbox"). +- Run this command again. +- Create a new org ("sf org create scratch" or "sf org create sandbox") and deploy the modified source. # error.packageDirectoryNeedsMainDefault -The package directory %s does not have a main/default structure. -The command will move metadata into main/default which doesn't seem like what you'd want. +The package directory %s doesn't have a main/default structure. +This command moves metadata into a main/default structure, but your package directories aren't ready for it. # error.packageDirectoryNeedsMainDefault.actions -- Update %s to have all its metadata inside main/default. +- Update %s to have all its metadata inside a main/default directory structure. - Run the command again. # success.dryRun -Files were created in %s outside your package directories for inspection. +Files were created in %s outside your package directories for you to inspect. # error.noTargetTypes -The project contains no packageDirectories with metadata that matches the specified behavior %s. +The project doesn't contain any package directories with metadata that matches the specified behavior %s. diff --git a/package.json b/package.json index 6823ed6c..a656e2d8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "description": "Commands to retrieve metadata from a Salesforce org" }, "convert": { - "description": "Commands to convert metadata from one format to another." + "description": "Commands to change the format of your project source files." }, "delete": { "description": "Commands to delete metadata from a Salesforce org." From eefa7398ceb952109fde387946958effc0a805df Mon Sep 17 00:00:00 2001 From: mshanemc Date: Fri, 24 May 2024 08:06:59 -0500 Subject: [PATCH 17/20] fix: change behavior shortchar --- src/commands/project/convert/source-behavior.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/project/convert/source-behavior.ts b/src/commands/project/convert/source-behavior.ts index 54c4f4d3..3874ec80 100644 --- a/src/commands/project/convert/source-behavior.ts +++ b/src/commands/project/convert/source-behavior.ts @@ -38,7 +38,7 @@ export default class ConvertSourceBehavior extends SfCommand Date: Fri, 24 May 2024 08:08:49 -0500 Subject: [PATCH 18/20] chore: snapshot/schema --- command-snapshot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command-snapshot.json b/command-snapshot.json index ac81543a..69009d71 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -40,7 +40,7 @@ "alias": [], "command": "project:convert:source-behavior", "flagAliases": [], - "flagChars": ["o", "p"], + "flagChars": ["b", "o"], "flags": ["behavior", "dry-run", "flags-dir", "json", "preserve-temp-dir", "target-org"], "plugin": "@salesforce/plugin-deploy-retrieve" }, From 6f95895dcdcaef9424112700242e786438e156e6 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Tue, 28 May 2024 15:50:06 -0500 Subject: [PATCH 19/20] Update messages/convert.source-behavior.md Co-authored-by: Willhoit --- messages/convert.source-behavior.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/convert.source-behavior.md b/messages/convert.source-behavior.md index d28657ac..8bc8d691 100644 --- a/messages/convert.source-behavior.md +++ b/messages/convert.source-behavior.md @@ -34,7 +34,7 @@ Display what the command would do, but don't make any actual changes. # flags.dry-run.description -Doesn't modify the existing files in your project, including the "sfdx-project.json" file. Instead, the command lists the files that would be deleted, explains the modifications to the "sfdx-project.json" file, and outputs the resulting modifications to a new directory for review. +Doesn't modify the existing files in your project, including the "sfdx-project.json" file. Instead, the command lists the files that would be deleted, explains the modifications to the "sfdx-project.json" file, and outputs the resulting modifications to a new directory named `DRY-RUN-RESULTS` for review. # flags.preserve-temp-dir.summary From 5490000464c7da3e239c6e0c317420daaf4bdb6b Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Wed, 29 May 2024 08:05:27 -0500 Subject: [PATCH 20/20] chore: pr suggestions Co-authored-by: Willhoit --- .github/workflows/test.yml | 4 ++-- messages/convert.source-behavior.md | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1f49a47..9510c0f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,10 +12,10 @@ jobs: needs: yarn-lockfile-check uses: salesforcecli/github-workflows/.github/workflows/unitTestsLinux.yml@main windows-unit-tests: - # needs: linux-unit-tests + needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main nuts: - # needs: linux-unit-tests + needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main secrets: inherit strategy: diff --git a/messages/convert.source-behavior.md b/messages/convert.source-behavior.md index 8bc8d691..eca4154b 100644 --- a/messages/convert.source-behavior.md +++ b/messages/convert.source-behavior.md @@ -6,7 +6,7 @@ Enable a behavior of your project source files, and then update your Salesforce Specifically, this command updates the "sourceBehaviorOption" option in the "sfdx-project.json" file and then converts the associated local source files in your project as needed. -For example, run this command with the "--behavior decomposePermissionSet" flag to start decomposing permission sets when you deploy or retrieve them. Decomposing means breaking up the monolithic metadata API format XML file that corresponds to a metadata component into smaller XML files and directories based on its subtypes. Permission sets are not decomposed by default; you must opt-in to start decomposing them by using this command. When the command finishes, your "sfdx-project.json" file is updated to always decompose permission sets, and the existing permission set files in your local package directories are converted into the new decomposed format. You run this command only once for a given behavior change. +For example, run this command with the "--behavior decomposePermissionSetBeta" flag to start decomposing permission sets when you deploy or retrieve them. Decomposing means breaking up the monolithic metadata API format XML file that corresponds to a metadata component into smaller XML files and directories based on its subtypes. Permission sets are not decomposed by default; you must opt-in to start decomposing them by using this command. When the command finishes, your "sfdx-project.json" file is updated to always decompose permission sets, and the existing permission set files in your local package directories are converted into the new decomposed format. You run this command only once for a given behavior change. For more information about the possible values for the --behavior flag, see the "sourceBehaviorOptions" section in the https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_ws_config.htm topic. @@ -18,15 +18,15 @@ Behavior to enable; the values correspond to the possible values of the "sourceB - Update your Salesforce DX project to decompose custom labels: - <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta - Display what the command would do, but don't change any existing files: - <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels --dry-run + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta --dry-run - Keep the temporary directory that contains the interim metadata API formatted files: - <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabels --dry-run --preserve-temp-dir + <%= config.bin %> <%= command.id %> --behavior decomposeCustomLabelsBeta --dry-run --preserve-temp-dir # flags.dry-run.summary