Skip to content

Commit e2ff339

Browse files
committed
feat: expand static resources with the proper file extension based on the contents mime type
1 parent 08f607f commit e2ff339

File tree

9 files changed

+110
-52
lines changed

9 files changed

+110
-52
lines changed

packages/salesforce/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@types/tough-cookie": "^4.0.2",
6565
"esbuild": "^0.25.4",
6666
"jest": "^29.7.0",
67+
"mime-db": "^1.54.0",
6768
"nugget": "^2.2.0",
6869
"shx": "^0.3.4",
6970
"ts-jest": "^29.3.4",

packages/salesforce/src/deploy/metadataExpander.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { SalesforcePackageComponentFile } from './package';
33
import { outputFile } from 'fs-extra';
44
import { MetadataRegistry, MetadataType } from '../metadataRegistry';
55
import { injectable } from '@vlocode/core';
6-
import { asArray, XML } from '@vlocode/util';
6+
import { asArray, substringBefore, XML } from '@vlocode/util';
7+
import * as mimeTypes from 'mime-db';
78

89
interface MetadataFile extends SalesforcePackageComponentFile {
9-
read(): Promise<Buffer>;
10+
content(): Promise<Buffer>;
11+
metadata(): Promise<(object & { "@type": string }) | undefined>;
1012
}
1113

1214
interface OutputFile {
@@ -15,22 +17,50 @@ interface OutputFile {
1517

1618
@injectable()
1719
export class MetadataExpander {
18-
public constructor(private metadataRegistry: MetadataRegistry) {
19-
}
2020

2121
public async expandMetadata(metadata: MetadataFile): Promise<Record<string, Buffer>> {
22-
const metadataContent = await metadata.read();
23-
const type = this.metadataRegistry.getMetadataType(metadata.componentType);
22+
const content = await metadata.content();
23+
const type = MetadataRegistry.getMetadataType(metadata.componentType);
2424

2525
if (type?.childXmlNames.length) {
26-
return this.expandMetadataChildren(metadata.componentName, type, XML.parse(metadataContent));
26+
return this.expandMetadataChildren(metadata.componentName, type, XML.parse(content));
27+
}
28+
29+
const metadataObj = await metadata.metadata();
30+
if (!metadataObj) {
31+
return this.expandContentAsMeta(metadata, content);
32+
}
33+
34+
if (metadataObj['@type'] === 'StaticResource') {
35+
return this.expandStaticResource(metadata, content, metadataObj);
2736
}
2837

29-
const basename = decodeURIComponent(path.basename(metadata.packagePath));
30-
const appendMetaXml = this.shouldAppendMetaXml(metadata.packagePath, metadataContent);
38+
return this.expandAsContent(metadata, content);
39+
}
40+
41+
private expandStaticResource(metadata: MetadataFile, content: Buffer, meta: object): Record<string, Buffer> {
42+
const basename = this.baseName(metadata.packagePath);
43+
const contentType = meta['contentType']?.toLowerCase();
44+
const mimeType = mimeTypes[contentType];
45+
const suffix = mimeType?.extensions?.[0] || 'resource';
46+
return {
47+
[`${basename}.${suffix}`]: content
48+
};
49+
}
50+
51+
private expandContentAsMeta(metadata: MetadataFile, content: Buffer): Record<string, Buffer> {
52+
const basename = this.baseName(metadata.packagePath);
53+
const appendMetaXml = this.shouldAppendMetaXml(metadata.packagePath, content);
3154
const expandedName = appendMetaXml ? `${basename}-meta.xml` : basename;
3255
return {
33-
[expandedName]: metadataContent
56+
[expandedName]: content
57+
};
58+
}
59+
60+
private expandAsContent(metadata: MetadataFile, content: Buffer): Record<string, Buffer> {
61+
const basename = this.baseName(metadata.packagePath);
62+
return {
63+
[basename]: content
3464
};
3565
}
3666

@@ -72,4 +102,9 @@ export class MetadataExpander {
72102
const bodyString = body.toString('utf8', 0, Math.min(100, body.length));
73103
return bodyString.includes('<?xml');
74104
}
105+
106+
private baseName(packagePath: string, removeSuffix?: boolean): string {
107+
const basename = decodeURIComponent(path.basename(packagePath))
108+
return removeSuffix ? substringBefore(basename, '.') : basename;
109+
}
75110
}

packages/salesforce/src/deploy/packageBuilder.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ export class SalesforcePackageBuilder {
126126
private readonly composedData = new Map<string, MetadataObject>();
127127
private readonly replacements = new Array<TokenReplacement>();
128128

129-
@injectable.property private readonly metadataRegistry: MetadataRegistry;
130129
@injectable.property private readonly logger: Logger;
131130

132131
constructor(
@@ -664,7 +663,7 @@ export class SalesforcePackageBuilder {
664663
}
665664

666665
private getMetadataType(xmlName: string) {
667-
const metadataTypes = this.metadataRegistry.getMetadataTypes();
666+
const metadataTypes = MetadataRegistry.getMetadataTypes();
668667
return metadataTypes.find(type => type.name == xmlName || type.childXmlNames?.includes(xmlName));
669668
}
670669

@@ -679,7 +678,7 @@ export class SalesforcePackageBuilder {
679678
const folder = pathParts.pop();
680679
const parentFolder = pathParts.pop();
681680

682-
for (const type of this.metadataRegistry.getMetadataTypes()) {
681+
for (const type of MetadataRegistry.getMetadataTypes()) {
683682
if (type.strictDirectoryName) {
684683
if (type.isBundle) {
685684
// match parent folder only for bundles
@@ -695,7 +694,7 @@ export class SalesforcePackageBuilder {
695694
// Suffix check
696695
const fileSuffix = this.getFileSuffix(file)?.toLocaleLowerCase();
697696
if (fileSuffix) {
698-
return this.metadataRegistry.getMetadataTypeBySuffix(fileSuffix)?.name;
697+
return MetadataRegistry.getMetadataTypeBySuffix(fileSuffix)?.name;
699698
}
700699
}
701700

@@ -719,7 +718,7 @@ export class SalesforcePackageBuilder {
719718
}
720719
}
721720

722-
const metadataTypes = this.metadataRegistry.getMetadataTypes();
721+
const metadataTypes = MetadataRegistry.getMetadataTypes();
723722
const xmlName = await this.getRootElementName(file);
724723

725724
// Cannot detect certain metadata types properly so instead manually set the type
@@ -827,7 +826,7 @@ export class SalesforcePackageBuilder {
827826

828827
private async getPackageComponentType(file: string, metadataType: MetadataType) {
829828
if (metadataType.folderContentType) {
830-
return this.metadataRegistry.getMetadataType(metadataType.folderContentType)!.name;
829+
return MetadataRegistry.getMetadataType(metadataType.folderContentType)!.name;
831830
}
832831
return metadataType.name;
833832
}

packages/salesforce/src/metadataRegistry.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import registryData from './registry/metadataRegistry.json';
22
import { MetadataType as RegistryMetadataType } from './registry/types';
3-
import { singletonMixin } from '@vlocode/util';
4-
import { injectable, Logger } from '@vlocode/core';
3+
import { injectable, Logger, LogManager } from '@vlocode/core';
54
import urlFormats from './metadataUrls.json';
65

76
export interface MetadataUrlFormat {
@@ -34,17 +33,14 @@ export interface MetadataType extends RegistryMetadataType {
3433
label: string;
3534
}
3635

37-
@singletonMixin
3836
@injectable.singleton()
39-
export class MetadataRegistry {
37+
class MetadataRegistryStore {
38+
readonly registry = new Array<MetadataType>();
39+
readonly types = new Map<string, MetadataType>();
40+
readonly suffixes = new Map<string, string>();
4041

41-
private readonly registry = new Array<MetadataType>();
42-
private readonly types = new Map<string, MetadataType>();
43-
private readonly urlFormats = new Map<string, MetadataUrlFormat>();
44-
private readonly suffixes = new Map<string, string>();
45-
46-
constructor(private readonly logger: Logger) {
47-
// Init metadata
42+
constructor(logger: Logger = LogManager.get('MetadataRegistry')) {
43+
// Init metadata registry
4844
for (const registryEntry of Object.values(registryData.types)) {
4945
const metadataObject = registryEntry as MetadataType;
5046

@@ -58,7 +54,7 @@ export class MetadataRegistry {
5854
// Store in registry
5955
this.registry.push(metadataObject);
6056
if (this.types.has(metadataObject.name.toLowerCase())) {
61-
this.logger.warn(`XML Name already in-use: ${metadataObject.name.toLowerCase()}`);
57+
logger.warn(`XML Name already in-use: ${metadataObject.name.toLowerCase()}`);
6258
continue;
6359
}
6460

@@ -79,7 +75,7 @@ export class MetadataRegistry {
7975
}
8076
}
8177

82-
/**
78+
/**
8379
* Converts a camelCase or PascalCase string to a proper label format
8480
* @param name The name to format
8581
* @returns The formatted label
@@ -100,22 +96,46 @@ export class MetadataRegistry {
10096
// Capitalize first letter and return
10197
return result.charAt(0).toUpperCase() + result.slice(1);
10298
}
99+
}
100+
101+
/**
102+
* Metadata registry for Salesforce metadata types.
103+
*
104+
* This namespace provides access to bundled metadata type information,
105+
* URL formats, and utility methods for working with Salesforce metadata.
106+
*
107+
* The latest metadata types are loaded from Salesforce when the package is build,
108+
* the build date determines the API version of the metadata registry.
109+
*/
110+
export namespace MetadataRegistry {
111+
112+
/**
113+
* Singleton instance of the metadata registry store
114+
* @returns {MetadataRegistryStore} The singleton instance of the metadata registry store
115+
*/
116+
export const store = new MetadataRegistryStore();
103117

104-
public getUrlFormat(type: string) {
118+
/**
119+
* Get the URL format for a given metadata type
120+
* @param type Metadata type name or XML name
121+
* @returns {MetadataUrlFormat} The URL format for the given metadata type
122+
* @throws {Error} If the metadata type is not found
123+
*/
124+
export function getUrlFormat(type: string) {
105125
return { ...urlFormats.$default, ...(urlFormats[type] ?? {}) };
106126
}
107127

108128
/**
109129
* Get the list of supported metadata types for the current organization merged with static metadata from the SFDX registry
110130
*/
111-
public getMetadataTypes() : MetadataType[] {
131+
export function getMetadataTypes() : MetadataType[] {
112132
return this.registry;
113133
}
114134

115135
/**
116136
* Get the list of supported metadata types for the current organization merged with static metadata from the SFDX registry
117137
*/
118-
public getMetadataSuffixes() : string[] {
138+
export function getMetadataSuffixes() : string[] {
119139
return [...Object.keys(registryData.suffixes)];
120140
}
121141

@@ -124,26 +144,26 @@ export class MetadataRegistry {
124144
* @param suffix File suffix without .
125145
* @returns
126146
*/
127-
public isMetadataSuffix(suffix: string) {
128-
return this.suffixes.has(suffix.toLowerCase());
147+
export function isMetadataSuffix(suffix: string) {
148+
return store.suffixes.has(suffix.toLowerCase());
129149
}
130150

131151
/**
132152
* Get the metadata type info based on XML name
133153
* @param type
134154
* @returns
135155
*/
136-
public getMetadataType(type: string) {
137-
return this.types.get(type.toLowerCase());
156+
export function getMetadataType(type: string) {
157+
return store.types.get(type.toLowerCase());
138158
}
139159

140160
/**
141161
* Get the primary Metadata type for a given file suffix
142162
* @param suffix File suffix without .
143163
* @returns
144164
*/
145-
public getMetadataTypeBySuffix(suffix: string) : MetadataType | undefined {
146-
const metadataType = this.suffixes.get(suffix.toLowerCase());
147-
return metadataType ? this.getMetadataType(metadataType) : undefined;
165+
export function getMetadataTypeBySuffix(suffix: string) : MetadataType | undefined {
166+
const metadataType = store.suffixes.get(suffix.toLowerCase());
167+
return metadataType ? getMetadataType(metadataType) : undefined;
148168
}
149169
}

packages/salesforce/src/retrieveDeltaStrategy.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export class RetrieveDeltaStrategy {
5454

5555
constructor(
5656
private readonly deployService: SalesforceDeployService,
57-
private readonly metadataRegistry: MetadataRegistry,
5857
private readonly logger: Logger) {
5958
}
6059

@@ -98,7 +97,7 @@ export class RetrieveDeltaStrategy {
9897
* @returns Returns true if the component has changed
9998
*/
10099
private async isComponentChanged(mdPackage: SalesforcePackage, component: RetrieveResultComponent) {
101-
const metadataType = this.metadataRegistry.getMetadataType(component.componentType);
100+
const metadataType = MetadataRegistry.getMetadataType(component.componentType);
102101
if (!metadataType) {
103102
return true;
104103
}

packages/salesforce/src/salesforceDeployService.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export class SalesforceDeployService {
3333
constructor(...args: any[]);
3434
constructor(
3535
private readonly salesforce: SalesforceConnectionProvider,
36-
private readonly metadataRegister: MetadataRegistry,
3736
private readonly logger: Logger) {
3837
}
3938

@@ -175,7 +174,7 @@ export class SalesforceDeployService {
175174

176175
while(types.length) {
177176
const typeName = types.shift()!;
178-
const metadataInfo = this.metadataRegister.getMetadataType(typeName);
177+
const metadataInfo = MetadataRegistry.getMetadataType(typeName);
179178
const currentGroup = metadataGroups.find(group => group.length < typesPerChunk)
180179
?? (metadataGroups[metadataGroups.length] = []);
181180

packages/salesforce/src/salesforceService.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ interface MetadataInfo { type: string; fullName: string; metadata: any; name: st
4747
@injectable({ lifecycle: LifecyclePolicy.singleton })
4848
export class SalesforceService implements SalesforceConnectionProvider {
4949

50-
@injectable.property public readonly metadataRegistry: MetadataRegistry;
5150
@injectable.property public readonly schema: SalesforceSchemaService;
5251
@injectable.property public readonly lookupService: SalesforceLookupService;
5352
@injectable.property public readonly deploy: SalesforceDeployService;
@@ -313,7 +312,7 @@ export class SalesforceService implements SalesforceConnectionProvider {
313312
if (metadataInfo?.fullName.includes('__mdt')) {
314313
metadataInfo.type = metadataInfo.type.replace('Custom', 'CustomMetadata');
315314
}
316-
const metadataUrlFormat = metadataInfo && this.metadataRegistry.getUrlFormat(metadataInfo.type);
315+
const metadataUrlFormat = metadataInfo && MetadataRegistry.getUrlFormat(metadataInfo.type);
317316

318317
if (!metadataInfo || !metadataUrlFormat) {
319318
throw new Error(`Unable to resolve metadata type (is this a valid Salesforce metadata?) for: ${file}`);
@@ -366,7 +365,7 @@ export class SalesforceService implements SalesforceConnectionProvider {
366365
const infos = await mapAsyncParallel(files, async file => {
367366
const info = sfPackage.getSourceFileInfo(file);
368367
if (info) {
369-
const metadataInfo = this.metadataRegistry.getMetadataType(info.componentType)!;
368+
const metadataInfo = MetadataRegistry.getMetadataType(info.componentType)!;
370369
const metadataFile = !file.endsWith('.xml') && metadataInfo.hasContent && metadataInfo.suffix
371370
? [...sfPackage.getComponentFiles(info)].find(f => f.packagePath.endsWith(`.${metadataInfo.suffix}-meta.xml`))?.fsPath
372371
: file;
@@ -527,14 +526,14 @@ export class SalesforceService implements SalesforceConnectionProvider {
527526
* Get the list of supported metadata types for the current organization merged with static metadata from the SFDX registry
528527
*/
529528
public getMetadataTypes() : MetadataType[] {
530-
return this.metadataRegistry.getMetadataTypes();
529+
return MetadataRegistry.getMetadataTypes();
531530
}
532531

533532
/**
534533
* When the metadata type is a known metadata type return the type.
535534
*/
536535
public getMetadataType(type: string) {
537-
return this.metadataRegistry.getMetadataType(type);
536+
return MetadataRegistry.getMetadataType(type);
538537
}
539538

540539
/**

packages/vscode-extension/src/lib/salesforce/metadataDetector.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,14 @@ import { MetadataRegistry } from '@vlocode/salesforce';
66

77
@injectable.singleton()
88
export class MetadataDetector {
9-
constructor(private readonly registry: MetadataRegistry) {
10-
}
11-
129
public isMetadataFile(file: FileFilterInfo | string) {
1310
if (typeof file === 'string') {
1411
return this.isMetadataFile({ fullName: file, folderName: directoryName(file), name: fileName(file), siblings: new Array<any>() } as FileFilterInfo);
1512
}
1613
if (file.name.endsWith('-meta.xml')) {
1714
return true;
1815
}
19-
const metadataInfo = this.registry.getMetadataTypeBySuffix(fileSuffix(file.name));
16+
const metadataInfo = MetadataRegistry.getMetadataTypeBySuffix(fileSuffix(file.name));
2017
if (metadataInfo) {
2118
return true;
2219
}

0 commit comments

Comments
 (0)