Skip to content

Commit

Permalink
feat: support new metadata types for deployment
Browse files Browse the repository at this point in the history
  • Loading branch information
Codeneos committed Aug 4, 2023
1 parent 9cd3afc commit f5a7139
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 266 deletions.
3 changes: 2 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node-linker=isolated
public-hoist-pattern[]['*eslint*', '*prettier*', '@oclif/command']
package-import-method=clone-or-copy
strict-peer-dependencies=false
prefer-workspace-packages=true
prefer-workspace-packages=true
shell-emulator=true
3 changes: 1 addition & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"pack": "pnpm run prepublish && pnpm run pack-only",
"prepublish": "pnpm run clean && pnpm run build-webpack && tsc",
"pack-only": "pnpm pack",
"clean": "shx rm -rf ./dist ./lib ./.ts-temp ./*.tgz ./src/**/*.d.ts ./src/**/*.d.ts.map"
"clean": "shx rm -rf ./dist ./lib ./.ts-temp './*.tgz' './src/**/*.d.ts' './src/**/*.d.ts.map'"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -62,7 +62,6 @@
"@vlocode/vlocity-deploy": "workspace:*",
"chalk": "^4.1.1",
"commander": "^9.2.0",
"create-ts-index": "^1.14.0",
"fs-extra": "^9.0",
"glob": "^7.1.7",
"jest": "^29.6.1",
Expand Down
14 changes: 6 additions & 8 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@
"lib": "./lib"
},
"engines": {
"node": ">=12.0.0",
"node": ">=16.0.0",
"npm": ">=6.0.0"
},
"scripts": {
"build": "pnpm run pre-build && tsc",
"clean": "shx rm -rf ./lib ./coverage ./tsconfig.tsbuildinfo ./src/**/*.d.ts ./src/**/*.d.ts.map",
"watch": "pnpm run pre-build && tsc -w",
"pack": "pnpm run build && pnpm pack",
"prepublish": "pnpm run build",
"pre-build": "cti -w -b ./src/fs ./src/logging/writers"
"build": "tsc",
"clean": "shx rm -rf ./lib ./coverage ./tsconfig.tsbuildinfo './src/**/*.d.ts' './src/**/*.d.ts.map'",
"watch": "tsc -w",
"pack": "pnpm pack",
"prepublish": "pnpm run build"
},
"repository": {
"type": "git",
Expand All @@ -43,7 +42,6 @@
"@types/jest": "^28.1.6",
"@types/luxon": "^3.1.0",
"@types/node": "^20.4.2",
"create-ts-index": "^1.14.0",
"jest": "^29.6.1",
"shx": "^0.3.4",
"ts-jest": "^29.1.1",
Expand Down
13 changes: 8 additions & 5 deletions packages/salesforce/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@
"engines": {
"node": ">=16.0.0"
},
"config": {
"metadata": "https://raw.githubusercontent.com/forcedotcom/source-deploy-retrieve/main/src/registry"
},
"scripts": {
"build": "pnpm run pre-build && tsc && tsc-alias",
"clean": "shx rm -rf ./lib ./coverage ./tsconfig.tsbuildinfo ./*.tgz ./src/**/*.d.ts ./src/**/*.d.ts.map",
"clean": "shx rm -rf ./lib ./coverage ./tsconfig.tsbuildinfo './*.tgz' './src/**/*.d.ts' './src/**/*.d.ts.map'",
"watch": "pnpm run pre-build && tsc -w & tsc-alias -w",
"pack": "pnpm run build && pnpm pack",
"prepublish": "pnpm run build",
"pre-build": "cti -w -b ./src -i constants"
"update-registry": "nugget $npm_package_config_metadata/metadataRegistry.json $npm_package_config_metadata/stdValueSetRegistry.json $npm_package_config_metadata/types.ts -q -d ./src/registry",
"pre-build": "pnpm update-registry",
"prepare": "pnpm update-registry"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -49,8 +54,8 @@
"@types/luxon": "^3.1.0",
"@types/node": "^20.4.2",
"@types/tough-cookie": "^4.0.2",
"create-ts-index": "^1.14.0",
"jest": "^29.6.1",
"nugget": "^2.2.0",
"shx": "^0.3.4",
"ts-jest": "^29.1.1",
"ts-node": "^10.4.0",
Expand All @@ -59,7 +64,6 @@
"webpack-env": "^0.8.0"
},
"dependencies": {
"@salesforce/source-deploy-retrieve": "^5",
"@vlocode/core": "workspace:*",
"@vlocode/util": "workspace:*",
"axios": "^0.25.0",
Expand All @@ -73,7 +77,6 @@
"log-symbols": "^4.0.0",
"luxon": "^3.1.0",
"moment": "^2.29.1",
"salesforce-alm": "^52.1.9",
"tough-cookie": "^4.1.2",
"xml2js": "^0.5.0"
},
Expand Down
128 changes: 85 additions & 43 deletions packages/salesforce/src/deploymentPackageBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export interface DeltaPackageStrategy<TOptions extends object = object> {
): Promise<Array<SalesforcePackageComponent>>;
}

interface MetadataObject {
type: MetadataType;
data: Record<string, unknown>;
}

@injectable( { lifecycle: LifecyclePolicy.transient } )
export class SalesforcePackageBuilder {

Expand All @@ -56,6 +61,7 @@ export class SalesforcePackageBuilder {
private readonly mdPackage: SalesforcePackage;
private readonly fs: FileSystem;
private readonly parsedFiles = new Set<string>();
private readonly composedData = new Map<string, MetadataObject>();
@injectable.property private readonly metadataRegistry: MetadataRegistry;
@injectable.property private readonly logger: Logger;

Expand Down Expand Up @@ -117,34 +123,35 @@ export class SalesforcePackageBuilder {
// Only Aura and LWC are bundled at this moment
// Classic metadata package all related files
await this.addBundledSources(path.dirname(file), metadataType);
} else {
// add source
await this.addSingleSourceFile(file, metadataType);
}

// add source
await this.addSingleSourceFile(file, metadataType);
}

for (const [file, xmlName, metadataType] of this.sortXmlFragments(childMetadataFiles)) {
await this.mergeChildSourceWithParent(file, xmlName, metadataType);
}

this.persistComposedMetadata();
return this;
}

private sortXmlFragments(fragments: Array<[string, string, MetadataType]>) : Array<[string, string, MetadataType]> {
return fragments.sort((a, b) => {
const metaTypeCompare = a[2].xmlName.localeCompare(b[2].xmlName);
return fragments.sort(([, fragmentTypeA, parentTypeA], [, fragmentTypeB, parentTypeB]) => {
const metaTypeCompare = parentTypeA.xmlName.localeCompare(parentTypeB.xmlName);
if (metaTypeCompare != 0) {
return metaTypeCompare;
}

const decompositions = a[2].decompositionConfig?.decompositions;
const decompositions = Object.values(parentTypeA.children?.types ?? []);
if (decompositions) {
const decompositionIndex1 = decompositions.findIndex(d => d.metadataName == a[1]);
const decompositionIndex2 = decompositions.findIndex(d => d.metadataName == b[1]);
return decompositionIndex1 - decompositionIndex2;
const decompositionIndexA = decompositions.findIndex(({name}) => name === fragmentTypeA);
const decompositionIndexB = decompositions.findIndex(({name}) => name === fragmentTypeB);
return decompositionIndexA - decompositionIndexB;
}

return a[1].localeCompare(b[1]);
return fragmentTypeA.localeCompare(fragmentTypeB);
});
}

Expand All @@ -153,13 +160,17 @@ export class SalesforcePackageBuilder {

// Build zip archive for all expanded file; filter out files already parsed
// Note use posix path separators when building package.zip
for (const file of Iterable.filter(fileNames, file => this.addParsedFile(file))) {
for (const file of fileNames) {

// Stop directly and return null
if (token && token.isCancellationRequested) {
break;
}

if (!this.addParsedFile(file)) {
continue;
}

// parse folders recursively
const fileStat = await this.fs.stat(file);
if (fileStat == null) {
Expand Down Expand Up @@ -213,9 +224,9 @@ export class SalesforcePackageBuilder {
const bundleFiles = await this.fs.readDirectory(bundleFolder);
const componentName = bundleFolder.split(/\\|\//g).pop()!;
for (const file of bundleFiles) {
if (file.isFile()) {
await this.addSingleSourceFile(path.join(bundleFolder, file.name), metadataType, componentName);
this.addParsedFile(path.join(bundleFolder, file.name));
const fullPath = path.join(bundleFolder, file.name);
if (file.isFile() && this.addParsedFile(fullPath)) {
await this.addSingleSourceFile(fullPath, metadataType, componentName);
}
}
}
Expand Down Expand Up @@ -292,55 +303,54 @@ export class SalesforcePackageBuilder {
/**
* Merge the source of the child element into the parent
* @param sourceFile Source file containing the child source
* @param xmlName XML name of the child element
* @param metadataType Metadata type of the parent/root
* @param fragmentTypeName XML name of the child element
* @param parentType Metadata type of the parent/root
*/
private async mergeChildSourceWithParent(sourceFile: string, xmlName: string, metadataType: MetadataType) {
private async mergeChildSourceWithParent(sourceFile: string, fragmentTypeName: string, parentType: MetadataType) {
// Get metadata type for a source file
const decomposition = metadataType.decompositionConfig?.decompositions.find(d => d.metadataName == xmlName);
if (!decomposition) {
this.logger.error(`No decomposition configuration for: ${chalk.green(sourceFile)} (${xmlName})`);
const fragmentType = Object.values(parentType.children?.types ?? []).find(d => d.name === fragmentTypeName);// ?.decompositions.find(d => d.metadataName == xmlName);
if (!fragmentType) {
this.logger.error(`No decomposition configuration for: ${chalk.green(sourceFile)} (${fragmentTypeName})`);
return;
}

const childComponentName = this.getPackageComponentName(sourceFile, metadataType);
const folderPerType = metadataType.strategies.decomposition == 'folderPerType';
const childComponentName = this.getPackageComponentName(sourceFile, parentType);
const folderPerType = parentType.strategies?.decomposition === 'folderPerType';

const parentComponentFolder = path.join(...sourceFile.split(/\\|\//g).slice(0, folderPerType ? -2 : -1));
const parentComponentName = path.basename(parentComponentFolder);
const parentComponentMetaFile = path.join(parentComponentFolder, `${parentComponentName}.${metadataType.suffix}-meta.xml`);
const parentPackagePath = await this.getPackagePath(parentComponentMetaFile, metadataType);
const parentComponentMetaFile = path.join(parentComponentFolder, `${parentComponentName}.${parentType.suffix}-meta.xml`);
const parentPackagePath = await this.getPackagePath(parentComponentMetaFile, parentType);

// Merge child metadata into parent metadata
if (this.type == SalesforcePackageType.deploy) {
await this.mergeMetadataFragment(parentPackagePath, sourceFile, metadataType);
await this.mergeMetadataFragment(parentPackagePath, sourceFile, parentType);
}

// Add as member to the package when not yet mentioned
if (this.type == SalesforcePackageType.destruct) {
this.mdPackage.addDestructiveChange(xmlName, `${parentComponentName}.${childComponentName}`);
this.mdPackage.addDestructiveChange(fragmentTypeName, `${parentComponentName}.${childComponentName}`);
} else {
if (decomposition.isAddressable) {
this.mdPackage.addManifestEntry(xmlName, `${parentComponentName}.${childComponentName}`);
if (fragmentType.isAddressable) {
this.mdPackage.addManifestEntry(fragmentTypeName, `${parentComponentName}.${childComponentName}`);
} else {
this.mdPackage.addManifestEntry(metadataType.xmlName, parentComponentName);
this.mdPackage.addManifestEntry(parentType.xmlName, parentComponentName);
}
this.mdPackage.addSourceMap(sourceFile, { componentType: xmlName, componentName: `${parentComponentName}.${childComponentName}`, packagePath: parentPackagePath });
this.mdPackage.addSourceMap(sourceFile, { componentType: fragmentTypeName, componentName: `${parentComponentName}.${childComponentName}`, packagePath: parentPackagePath });
}

this.logger.verbose(`Adding ${path.basename(sourceFile)} as ${parentComponentName}.${childComponentName}`);
}

/**
* Merge the source file into the existing package metadata when there is an existing metadata file.
* @param packagePath Relative path of the source file in the package
* @param fileToMerge Path of the metedata file to merge into the existing package file
* @param metadataType Type of metadata to merge
* @param xmlFragmentName Name of the XML fragement tag to use
* @param packagePath Path of the parent package file in the package
* @param fragmentFile Path of the metedata file on the FS that should be merged into the package
* @param metadataType Type of the metadata to merge
*/
private async mergeMetadataFragment(packagePath: string, fragmentFile: string, metadataType: MetadataType) {
const [[fragmentTag, fragmentMetadata]] = Object.entries(XML.parse(await this.fs.readFileAsString(fragmentFile), { trimValues: true }));
const decomposition = metadataType.decompositionConfig?.decompositions.find(d => d.metadataName == fragmentTag);
const decomposition = Object.values(metadataType.children?.types ?? []).find(d => d.name === fragmentTag);

if (!decomposition) {
this.logger.error(`No decomposition configuration for: ${chalk.green(fragmentFile)} (${fragmentTag})`);
Expand All @@ -357,11 +367,43 @@ export class SalesforcePackageBuilder {
existingPackageData = await this.fs.readFileAsString(parentSourceFile);
}
}
const existingMetadata = existingPackageData ? XML.parse(existingPackageData, { trimValues: true })[metadataType.xmlName] : {};

// Merge child metadata into parent metadata
const mergedMetadata = this.mergeMetadata(existingMetadata, { [decomposition.xmlFragmentName]: fragmentMetadata });
this.mdPackage.setPackageData(packagePath, { data: this.buildMetadataXml(metadataType.xmlName, mergedMetadata), componentType: metadataType.xmlName, componentName: path.basename(packagePath, `.${metadataType.suffix}`) });
const metadata = await this.readComposedMetadata(packagePath, fragmentFile, metadataType);
this.mergeMetadata(metadata[metadataType.xmlName], { [decomposition.directoryName]: fragmentMetadata });
}

private async readComposedMetadata(packagePath: string, fragmentFile: string, metadataType: MetadataType): Promise<any> {
const composedData = this.composedData.get(packagePath);
if (composedData) {
return composedData.data;
}

let existingPackageData = await this.readPackageData(packagePath);
if (!existingPackageData) {
// ensure parent files are included
const parentBaseName = path.posix.basename(packagePath);
const parentSourceFile = path.posix.join(fragmentFile, '..', '..', `${parentBaseName}-meta.xml`);
if (await this.fs.pathExists(parentSourceFile)) {
existingPackageData = await this.fs.readFileAsString(parentSourceFile);
}
}

const existingMetadata = existingPackageData
? XML.parse(existingPackageData, { trimValues: true })
: { [metadataType.xmlName]: {} };
this.composedData.set(packagePath, { data: existingMetadata, type: metadataType });
return existingMetadata;
}

private async persistComposedMetadata() {
for (const [ packagePath, { data, type } ] of this.composedData.entries()) {
this.mdPackage.setPackageData(packagePath, {
data: this.buildMetadataXml(type.xmlName, data[type.xmlName]),
componentType: type.xmlName,
componentName: path.basename(packagePath, `.${type.suffix}`)
});
}
}

private async readPackageData(packagePath: string): Promise<string | Buffer | undefined> {
Expand Down Expand Up @@ -573,16 +615,16 @@ export class SalesforcePackageBuilder {

private async getPackagePath(file: string, metadataType: MetadataType) {
const baseName = file.split(/\\|\//g).pop()!;
const baseNameSource = baseName.replace(/-meta\.xml$/i,'');
const contentName = baseName.replace(/-meta\.xml$/i,'');

const isMetaFile = baseName != baseNameSource;
const isMetaFile = baseName !== contentName;
const packageFolder = this.getPackageFolder(file, metadataType);
const expectedSuffix = isMetaFile ? `${metadataType.suffix}-meta.xml` : `${metadataType.suffix}`;

if (isMetaFile && !metadataType.metaFile && !metadataType.hasContent) {
if (isMetaFile && !metadataType.hasContent && !metadataType.isBundle) {
// SFDX adds a '-meta.xml' to each file, when deploying we need to strip these
// when the source does not have a meta data file
return path.posix.join(packageFolder, baseName.slice(0, -9));
return path.posix.join(packageFolder, contentName);
}

if (metadataType.id == 'document') {
Expand All @@ -595,7 +637,7 @@ export class SalesforcePackageBuilder {
}
} else if (metadataType.suffix && !file.endsWith(expectedSuffix)) {
// for non-document source files should match the metadata suffix
return path.posix.join(packageFolder, `${this.stripFileExtension(baseNameSource, 1)}.${expectedSuffix}`);
return path.posix.join(packageFolder, `${this.stripFileExtension(contentName, 1)}.${expectedSuffix}`);
}

return path.posix.join(packageFolder, baseName);
Expand Down
Loading

0 comments on commit f5a7139

Please sign in to comment.