Add folder for SDK style sql projects (#17918)

* update add folder for sdk style sql projects

* fix tests

* add test for add folder to sdk style project

* handle nested folders

* update helper function name
This commit is contained in:
Kim Santiago
2021-12-16 09:35:29 -08:00
committed by GitHub
parent 7378d56bfb
commit 703a925a92
3 changed files with 208 additions and 27 deletions

View File

@@ -238,8 +238,13 @@ export class Project implements ISqlProject {
private async readFolders(): Promise<FileProjectEntry[]> {
const folderEntries: FileProjectEntry[] = [];
// glob style getting folders for sdk style projects
const foldersSet = new Set<string>();
// get any folders listed in the project file
const sqlprojFolders = await this.foldersListedInSqlproj();
sqlprojFolders.forEach(f => foldersSet.add(f));
// glob style getting folders for sdk style projects
if (this._isSdkStyleProject) {
this.files.forEach(file => {
// if file is in the project's folder, add the folders from the project file to this file to the list of folders. This is so that only non-empty folders in the project folder will be added by default.
@@ -249,8 +254,29 @@ export class Project implements ISqlProject {
foldersToFile.forEach(f => foldersSet.add(utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(f)))));
}
});
// add any intermediate folders of the folders that are listed in the sqlproj
// If there are nested empty folders, there will only be a Folder entry for the inner most folder, so we need to add entries for the intermediate folders
sqlprojFolders.forEach(folder => {
const fullPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(folder));
const intermediateFolders = utils.getFoldersAlongPath(this.projectFolderPath, utils.getPlatformSafeFileEntryPath(fullPath));
intermediateFolders.forEach(f => foldersSet.add(utils.convertSlashesForSqlProj(utils.trimUri(Uri.file(this.projectFilePath), Uri.file(f)))));
});
}
foldersSet.forEach(f => {
folderEntries.push(this.createFileProjectEntry(f, EntryType.Folder));
});
return folderEntries;
}
/**
* @returns Array of folders specified in the sqlproj
*/
private async foldersListedInSqlproj(): Promise<string[]> {
const folders: string[] = [];
// get any folders listed in the project file
for (let ig = 0; ig < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) {
const itemGroup = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.ItemGroup)[ig];
@@ -262,8 +288,7 @@ export class Project implements ISqlProject {
// don't add Properties folder since it isn't supported for now and don't add if the folder was already added
if (utils.trimChars(relativePath, '\\') !== constants.Properties) {
// make sure folder relative path ends with \\ because sometimes SSDT adds folders without trailing \\
relativePath = relativePath.endsWith(constants.SqlProjPathSeparator) ? relativePath : relativePath + constants.SqlProjPathSeparator;
foldersSet.add(relativePath);
folders.push(utils.ensureTrailingSlash(relativePath));
}
}
} catch (e) {
@@ -272,11 +297,7 @@ export class Project implements ISqlProject {
}
}
foldersSet.forEach(f => {
folderEntries.push(this.createFileProjectEntry(f, EntryType.Folder));
});
return folderEntries;
return folders;
}
private readPreDeployScripts(): FileProjectEntry[] {
@@ -875,7 +896,7 @@ export class Project implements ISqlProject {
return outputItemGroup;
}
private async addFileToProjFile(path: string, xmlTag: string, attributes?: Map<string, string>): Promise<void> {
private async addFileToProjFile(filePath: string, xmlTag: string, attributes?: Map<string, string>): Promise<void> {
let itemGroup;
if (xmlTag === constants.PreDeploy || xmlTag === constants.PostDeploy) {
@@ -888,11 +909,25 @@ export class Project implements ISqlProject {
}
}
else {
if (this.isSdkStyleProject) {
// if there's a folder entry for the folder containing this file, remove it from the sqlproj because the folder will now be
// included by the glob that includes this file (same as how csproj does it)
const folders = await this.foldersListedInSqlproj();
folders.forEach(folder => {
const trimmedUri = utils.trimUri(Uri.file(utils.getPlatformSafeFileEntryPath(folder)), Uri.file(utils.getPlatformSafeFileEntryPath(filePath)));
const basename = path.basename(utils.getPlatformSafeFileEntryPath(filePath));
if (trimmedUri === basename) {
// remove folder entry from sqlproj
this.removeFolderNode(folder);
}
});
}
const currentFiles = await this.readFilesInProject();
// don't need to add an entry if it's already included by a glob pattern
// unless it has an attribute that needs to be added, like external streaming job which needs it so it can be determined if validation can run on it
if (attributes?.size === 0 && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(path))) {
if (attributes?.size === 0 && currentFiles.find(f => f.relativePath === utils.convertSlashesForSqlProj(filePath))) {
return;
}
@@ -901,7 +936,7 @@ export class Project implements ISqlProject {
const newFileNode = this.projFileXmlDoc!.createElement(xmlTag);
newFileNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(path));
newFileNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(filePath));
if (attributes) {
for (const key of attributes.keys()) {
@@ -979,22 +1014,29 @@ export class Project implements ISqlProject {
return false;
}
private addFolderToProjFile(path: string): void {
private async addFolderToProjFile(folderPath: string): Promise<void> {
if (this.isSdkStyleProject) {
// if there's a folder entry for the folder containing this folder, remove it from the sqlproj because the folder will now be
// included by the glob that includes this folder (same as how csproj does it)
const folders = await this.foldersListedInSqlproj();
folders.forEach(folder => {
const trimmedUri = utils.trimChars(utils.trimUri(Uri.file(utils.getPlatformSafeFileEntryPath(folder)), Uri.file(utils.getPlatformSafeFileEntryPath(folderPath))), '/');
const basename = path.basename(utils.getPlatformSafeFileEntryPath(folderPath));
if (trimmedUri === basename) {
// remove folder entry from sqlproj
this.removeFolderNode(folder);
}
});
}
const newFolderNode = this.projFileXmlDoc!.createElement(constants.Folder);
newFolderNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(path));
newFolderNode.setAttribute(constants.Include, utils.convertSlashesForSqlProj(folderPath));
this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode);
}
private async removeFolderFromProjFile(folderPath: string): Promise<void> {
const folderNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Folder);
let deleted = this.removeNode(folderPath, folderNodes);
// if it wasn't deleted, try deleting the folder path without trailing backslash
// since sometimes SSDT adds folders without a trailing \
if (!deleted) {
deleted = this.removeNode(utils.trimChars(folderPath, '\\'), folderNodes);
}
let deleted = this.removeFolderNode(folderPath);
// TODO: consider removing this check when working on migration scenario. If a user converts to an SDK-style project and adding this
// exclude XML doesn't hurt for non-SDK-style projects, then it might be better to just it anyway so that they don't have to exclude the folder
@@ -1025,6 +1067,19 @@ export class Project implements ISqlProject {
}
}
private removeFolderNode(folderPath: string): boolean {
const folderNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Folder);
let deleted = this.removeNode(folderPath, folderNodes);
// if it wasn't deleted, try deleting the folder path without trailing backslash
// since sometimes SSDT adds folders without a trailing \
if (!deleted) {
deleted = this.removeNode(utils.trimChars(folderPath, '\\'), folderNodes);
}
return deleted;
}
private async writeToSqlProjAndUpdateFilesFolders(): Promise<void> {
await this.serializeToProjFile(this.projFileXmlDoc);
const projFileText = await fs.readFile(this._projectFilePath);
@@ -1289,7 +1344,7 @@ export class Project implements ISqlProject {
await this.addFileToProjFile((<FileProjectEntry>entry).relativePath, xmlTag ? xmlTag : constants.Build, attributes);
break;
case EntryType.Folder:
this.addFolderToProjFile((<FileProjectEntry>entry).relativePath);
await this.addFolderToProjFile((<FileProjectEntry>entry).relativePath);
break;
case EntryType.DatabaseReference:
await this.addDatabaseReferenceToProjFile(<IDatabaseReferenceProjectEntry>entry);
@@ -1444,9 +1499,18 @@ export class Project implements ISqlProject {
// If folder doesn't exist, create it
await fs.mkdir(absoluteFolderPath, { recursive: true });
// don't need to add the folder to the sqlproj if this is an sdk style project because globbing will get the folders
// for SDK style projects, only add this folder to the sqlproj if needed
// intermediate folders don't need to be added in the sqlproj
if (this.isSdkStyleProject) {
return this.createFileProjectEntry(relativeFolderPath, EntryType.Folder);
let folderEntry = this.files.find(f => utils.ensureTrailingSlash(f.relativePath.toUpperCase()) === utils.ensureTrailingSlash((relativeFolderPath.toUpperCase())));
if (!folderEntry) {
folderEntry = this.createFileProjectEntry(utils.ensureTrailingSlash(relativeFolderPath), EntryType.Folder);
this.files.push(folderEntry);
await this.addToProjFile(folderEntry);
}
return folderEntry;
}
// Add project file entries for all folders in the path.