Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: warn about legacy suffix use #1298

Merged
merged 8 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/convert/convertContext/recompositionFinalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ const ensureStateValueWithParent = (
);
};

/** throw if the child has no parent component */
const ensureMetadataComponentWithParent = (
child: MetadataComponent
): child is SourceComponent & { parent: SourceComponent } => {
Expand Down
6 changes: 5 additions & 1 deletion src/convert/transformers/defaultMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { basename, dirname, join } from 'node:path';
import { Messages } from '@salesforce/core';
import { Messages } from '@salesforce/core/messages';
import { Lifecycle } from '@salesforce/core/lifecycle';
import { SourcePath } from '../../common/types';
import { META_XML_SUFFIX } from '../../common/constants';
import { SfdxFileFormat, WriteInfo } from '../types';
Expand Down Expand Up @@ -118,6 +119,9 @@ const getXmlDestination = (
}
}
if (legacySuffix && suffix && xmlDestination.includes(legacySuffix)) {
void Lifecycle.getInstance().emitWarning(
`The ${component.type.name} component ${component.fullName} uses the legacy suffix ${legacySuffix}. This suffix is deprecated and will be removed in a future release.`
);
xmlDestination = xmlDestination.replace(legacySuffix, suffix);
}
return xmlDestination;
Expand Down
30 changes: 24 additions & 6 deletions src/resolve/metadataResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,20 +385,38 @@ const resolveTypeFromStrictFolder =
.filter(folderTypeFilter(fsPath))
.find(
(type) =>
// any of the following 3 options is considered a good match
isMixedContentOrBundle(type) || suffixMatches(type, fsPath) || childSuffixMatches(type, fsPath)
// any of the following options is considered a good match
isMixedContentOrBundle(type) ||
suffixMatches(type, fsPath) ||
childSuffixMatches(type, fsPath) ||
legacySuffixMatches(type, fsPath)
);
};

/** the type has children and the file suffix (in source format) matches a child type suffix of the type we think it is */
const childSuffixMatches = (type: MetadataType, fsPath: string): boolean =>
Object.values(type.children?.types ?? {})
.map((childType) => `${childType.suffix}${META_XML_SUFFIX}`)
.some((s) => fsPath.endsWith(s));
Object.values(type.children?.types ?? {}).some(
(childType) => suffixMatches(childType, fsPath) || legacySuffixMatches(childType, fsPath)
);

/** the file suffix (in source or mdapi format) matches the type suffix we think it is */
const suffixMatches = (type: MetadataType, fsPath: string): boolean =>
typeof type.suffix === 'string' && [type.suffix, `${type.suffix}${META_XML_SUFFIX}`].some((s) => fsPath.endsWith(s));
typeof type.suffix === 'string' &&
(fsPath.endsWith(type.suffix) || fsPath.endsWith(appendMetaXmlSuffix(type.suffix)));

const legacySuffixMatches = (type: MetadataType, fsPath: string): boolean => {
if (
typeof type.legacySuffix === 'string' &&
(fsPath.endsWith(type.legacySuffix) || fsPath.endsWith(appendMetaXmlSuffix(type.legacySuffix)))
) {
void Lifecycle.getInstance().emitWarning(
`The ${type.name} component at ${fsPath} uses the legacy suffix ${type.legacySuffix}. This suffix is deprecated and will be removed in a future release.`
);
return true;
}
return false;
};
const appendMetaXmlSuffix = (suffix: string): string => `${suffix}${META_XML_SUFFIX}`;

const isMixedContentOrBundle = (type: MetadataType): boolean =>
typeof type.strategies?.adapter === 'string' && ['mixedContent', 'bundle'].includes(type.strategies.adapter);
Expand Down
17 changes: 12 additions & 5 deletions src/resolve/sourceComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { join, dirname } from 'node:path';
import { Messages, SfError } from '@salesforce/core';
import { SfError } from '@salesforce/core/sfError';
import { Messages } from '@salesforce/core/messages';
import { Lifecycle } from '@salesforce/core/lifecycle';

import { XMLParser, XMLValidator } from 'fast-xml-parser';
import { get, getString, JsonMap } from '@salesforce/ts-types';
import { ensureArray } from '@salesforce/kit';
Expand Down Expand Up @@ -308,14 +311,18 @@ export class SourceComponent implements MetadataComponent {
const children: SourceComponent[] = [];
for (const fsPath of this.walk(dirPath)) {
const childXml = parseMetadataXml(fsPath);
const fileIsRootXml = childXml?.suffix === this.type.suffix;
const fileIsRootXml = childXml?.suffix === this.type.suffix || childXml?.suffix === this.type.legacySuffix;
if (childXml && !fileIsRootXml && this.type.children && childXml.suffix) {
// TODO: Log warning if missing child type definition
const childTypeId = this.type.children?.suffixes[childXml.suffix];
const childSuffix = this.type.children.types[childTypeId]?.suffix;
const childType = this.type.children.types[childTypeId];
if (!childTypeId || !childType) {
void Lifecycle.getInstance().emitWarning(
`${fsPath}: Expected a child type for ${childXml.suffix} in ${this.type.name} but none was found.`
);
}
const childComponent = new SourceComponent(
{
name: childSuffix ? baseWithoutSuffixes(fsPath, childSuffix) : baseName(fsPath),
name: childType?.suffix ? baseWithoutSuffixes(fsPath, childType) : baseName(fsPath),
type: this.type.children.types[childTypeId],
xml: fsPath,
parent: this,
Expand Down
15 changes: 8 additions & 7 deletions src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,19 @@ export function baseName(fsPath: SourcePath): string {

/**
* the above baseName function doesn't handle components whose names have a `.` in them.
* this will handle that, but requires you to specify the expected suffix from the mdType.
* this will handle that, but requires you to specify the mdType to check suffixes for.
*
* @param fsPath The path to evaluate
*/
export function baseWithoutSuffixes(fsPath: SourcePath, suffix: string): string {
return basename(fsPath)
.replace(META_XML_SUFFIX, '')
.split('.')
.filter((part) => part !== suffix)
.join('.');
export function baseWithoutSuffixes(fsPath: SourcePath, mdType: MetadataType): string {
return basename(fsPath).replace(META_XML_SUFFIX, '').split('.').filter(stringIsNotSuffix(mdType)).join('.');
}

const stringIsNotSuffix =
(type: MetadataType) =>
(part: string): boolean =>
part !== type.suffix && (!type.legacySuffix || part !== type.legacySuffix);

/**
* Get the name of file path extension. Different from path.extname in that it
* does not include the '.' in the extension name. Returns an empty string if
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description>Big Object representation of Logger data, used as an alternative to the platform event LogEntryEvent__e, as well as a way to archive Logger data stored in Log__c, LogEntry__, and LogEntryTag__c</description>
<label>Log Entry Archive</label>
<pluralLabel>Log Entry Archives</pluralLabel>
<fields>
<fullName>Timestamp__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Timestamp</label>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>DateTime</type>
</fields>
<fields>
<fullName>TransactionEntryNumber__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<description>The sequential number of this log entry within the transaction</description>
<externalId>false</externalId>
<label>Entry #</label>
<precision>10</precision>
<required>true</required>
<scale>0</scale>
<securityClassification>Confidential</securityClassification>
<type>Number</type>
<unique>false</unique>
</fields>
<fields>
<fullName>TransactionId__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Transaction ID</label>
<length>36</length>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>Text</type>
<unique>false</unique>
</fields>
<indexes>
<fullName>LogEntryArchiveIndex</fullName>
<fields>
<name>Timestamp__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<fields>
<name>TransactionId__c</name>
<sortDirection>ASC</sortDirection>
</fields>
<fields>
<name>TransactionEntryNumber__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<label>Log Entry Archive Index</label>
</indexes>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>LogEntryArchive__b.Timestamp__c</members>
<members>LogEntryArchive__b.TransactionEntryNumber__c</members>
<members>LogEntryArchive__b.TransactionId__c</members>
<name>CustomField</name>
</types>
<types>
<members>LogEntryArchive__b</members>
<name>CustomObject</name>
</types>
<types>
<members>LogEntryArchive__b.LogEntryArchiveIndex</members>
<name>Index</name>
</types>
<version>60.0</version>
</Package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<deploymentStatus>Deployed</deploymentStatus>
<description
>Big Object representation of Logger data, used as an alternative to the platform event LogEntryEvent__e, as well as a way to archive Logger data stored in Log__c, LogEntry__, and LogEntryTag__c</description>
<label>Log Entry Archive</label>
<pluralLabel>Log Entry Archives</pluralLabel>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Timestamp__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Timestamp</label>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>DateTime</type>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>TransactionEntryNumber__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<description>The sequential number of this log entry within the transaction</description>
<externalId>false</externalId>
<label>Entry #</label>
<precision>10</precision>
<required>true</required>
<scale>0</scale>
<securityClassification>Confidential</securityClassification>
<type>Number</type>
<unique>false</unique>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>TransactionId__c</fullName>
<businessStatus>Active</businessStatus>
<complianceGroup>None</complianceGroup>
<externalId>false</externalId>
<label>Transaction ID</label>
<length>36</length>
<required>true</required>
<securityClassification>Confidential</securityClassification>
<type>Text</type>
<unique>false</unique>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Index xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>LogEntryArchiveIndex</fullName>
<fields>
<name>Timestamp__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<fields>
<name>TransactionId__c</name>
<sortDirection>ASC</sortDirection>
</fields>
<fields>
<name>TransactionEntryNumber__c</name>
<sortDirection>DESC</sortDirection>
</fields>
<label>Log Entry Archive Index</label>
</Index>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"packageDirectories": [
{
"default": true,
"path": "force-app"
}
],
"sourceApiVersion": "59.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 * as fs from 'node:fs';
import * as path from 'node:path';
import { MDAPI_OUT, fileSnap, sourceToMdapi } from '../../helper/conversions';

// we don't want failing tests outputting over each other
/* eslint-disable no-await-in-loop */

describe('legacy suffix support (.indexe for bigObject index)', () => {
const testDir = path.join('test', 'snapshot', 'sampleProjects', 'legacySuffixSupport');
let mdFiles: string[];

before(async () => {
mdFiles = await sourceToMdapi(testDir);
});

it('verify md files', async () => {
for (const file of mdFiles) {
await fileSnap(file, testDir);
}
});

after(async () => {
await Promise.all([fs.promises.rm(path.join(testDir, MDAPI_OUT), { recursive: true, force: true })]);
});
});
39 changes: 33 additions & 6 deletions test/utils/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { join } from 'node:path';
import { expect } from 'chai';
import { META_XML_SUFFIX } from '../../src/common';
import { parseMetadataXml, trimUntil, baseName, parseNestedFullName, baseWithoutSuffixes } from '../../src/utils';
import { MetadataType } from '../../src/registry/types';

describe('Path Utils', () => {
const root = join('path', 'to', 'whatever');
Expand All @@ -25,31 +26,57 @@ describe('Path Utils', () => {
});

describe('baseWithoutSuffixes', () => {
const mdTypeCommon: MetadataType = {
id: 'test',
name: 'Test',
directoryName: 'tests',
};
const mdType: MetadataType = {
...mdTypeCommon,
suffix: 'xyz',
};
const mdTypeLegacySuffix: MetadataType = {
...mdType,
suffix: 'xyz',
legacySuffix: 'xyzz',
};

it('Should strip specified suffixes from a file path with a dot', () => {
const path = join(root, 'a.ext.xyz');
expect(baseWithoutSuffixes(path, 'xyz')).to.equal('a.ext');
expect(baseWithoutSuffixes(path, mdType)).to.equal('a.ext');
});

it('Should strip specified suffixes from a file path with a dot and standard ending', () => {
const path = join(root, `a.ext.xyz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, 'xyz')).to.equal('a.ext');
expect(baseWithoutSuffixes(path, mdType)).to.equal('a.ext');
});

it('Should handle paths with no suffixes', () => {
const path = join(root, 'a');
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a');
expect(baseWithoutSuffixes(path, mdTypeCommon)).to.equal('a');
});

it('Should preserve non-matching suffixes', () => {
const path = join(root, 'a.xyz');
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a.xyz');
expect(baseWithoutSuffixes(path, mdTypeCommon)).to.equal('a.xyz');
});

it('Should remove the standard suffix and a custom suffix', () => {
const path = join(root, `a.ext${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, 'ext')).to.equal('a');
const path = join(root, `a.xyz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, mdType)).to.equal('a');
});

it('should remove a legacy suffix', () => {
const path = join(root, 'a.xyzz');
expect(baseWithoutSuffixes(path, mdTypeLegacySuffix)).to.equal('a');
});

it('should remove a legacy suffix with the standard meta', () => {
const path = join(root, `a.xyzz${META_XML_SUFFIX}`);
expect(baseWithoutSuffixes(path, mdTypeLegacySuffix)).to.equal('a');
});
});

describe('trimUntil', () => {
it('should return given path if part is not found', () => {
expect(trimUntil(root, 'test')).to.equal(root);
Expand Down
Loading