diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 6a10302e04..d57b5ade36 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -22,6 +22,7 @@ export const msdbDacpac = 'msdb.dacpac'; export const MicrosoftDatatoolsSchemaSqlSql = 'Microsoft.Data.Tools.Schema.Sql.Sql'; export const databaseSchemaProvider = 'DatabaseSchemaProvider'; export const sqlProjectSdk = 'Microsoft.Build.Sql'; +export const sqlProjectSdkVersion = '0.1.3-preview'; // Project Provider export const emptySqlDatabaseProjectTypeId = 'EmptySqlDbProj'; @@ -100,6 +101,7 @@ export function deleteConfirmationContents(toDelete: string) { return localize(' export function deleteReferenceConfirmation(toDelete: string) { return localize('deleteReferenceConfirmation', "Are you sure you want to delete the reference to {0}?", toDelete); } export function selectTargetPlatform(currentTargetPlatform: string) { return localize('selectTargetPlatform', "Current target platform: {0}. Select new target platform", currentTargetPlatform); } export function currentTargetPlatform(projectName: string, currentTargetPlatform: string) { return localize('currentTargetPlatform', "Target platform of the project {0} is now {1}", projectName, currentTargetPlatform); } +export function projectUpdatedToSdkStyle(projectName: string) { return localize('projectUpdatedToSdkStyle', "The project {0} has been updated to be an SDK-style project. Click 'Learn More' for details on the Microsoft.Build.Sql SDK and ways to simplify the project file.", projectName); } // Publish dialog strings diff --git a/extensions/sql-database-projects/src/controllers/projectController.ts b/extensions/sql-database-projects/src/controllers/projectController.ts index e560b5ca1f..b77d0895e2 100644 --- a/extensions/sql-database-projects/src/controllers/projectController.ts +++ b/extensions/sql-database-projects/src/controllers/projectController.ts @@ -862,6 +862,24 @@ export class ProjectsController { } } + /** + * Converts a legacy style project to an SDK-style project + * @param context a treeItem in a project's hierarchy, to be used to obtain a Project + */ + public async convertToSdkStyleProject(context: dataworkspace.WorkspaceTreeItem): Promise { + const project = this.getProjectFromContext(context); + + await project.convertProjectToSdkStyle(); + void this.reloadProject(context); + + // show message that project file can be simplified + const result = await vscode.window.showInformationMessage(constants.projectUpdatedToSdkStyle(project.projectFileName), constants.learnMore); + + if (result === constants.learnMore) { + void vscode.env.openExternal(vscode.Uri.parse(constants.sdkLearnMoreUrl!)); + } + } + /** * Adds a database reference to the project * @param context a treeItem in a project's hierarchy, to be used to obtain a Project diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index e67b3840df..e01a78bbff 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -617,6 +617,45 @@ export class Project implements ISqlProject { await this.createCleanFileNode(beforeBuildNode); } + public async convertProjectToSdkStyle(): Promise { + // don't do anything if the project is already SDK style or it's an SSDT project that hasn't been updated to build in ADS + if (this.isSdkStyleProject || !this._importedTargets.includes(constants.NetCoreTargets)) { + return; + } + + // make backup copy of project + await fs.copyFile(this._projectFilePath, this._projectFilePath + '_backup'); + + // remove SSDT and ADS SqlTasks imports + const importsToRemove = []; + for (let i = 0; i < this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import).length; i++) { + const importTarget = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Import)[i]; + const projectAttributeVal = importTarget.getAttribute(constants.Project); + + if (projectAttributeVal === constants.NetCoreTargets || projectAttributeVal === constants.SqlDbTargets || projectAttributeVal === constants.MsBuildtargets) { + importsToRemove.push(importTarget); + } + } + + const parent = importsToRemove[0]?.parentNode; + importsToRemove.forEach(i => { parent?.removeChild(i); }); + + // add SDK node + const sdkNode = this.projFileXmlDoc!.createElement(constants.Sdk); + sdkNode.setAttribute(constants.Name, constants.sqlProjectSdk); + sdkNode.setAttribute(constants.Version, constants.sqlProjectSdkVersion); + + const projectNode = this.projFileXmlDoc!.documentElement; + projectNode.insertBefore(sdkNode, projectNode.firstChild); + + // TODO: also update system dacpac path, but might as well wait for them to get included in the SDK since the path will probably change again + + // TODO: remove Build includes and folder includes. Make sure the same files and folders are being included and there aren't extra files included by the default **/*.sql glob + + await this.serializeToProjFile(this.projFileXmlDoc!); + await this.readProjFile(); + } + private async createCleanFileNode(parentNode: Element): Promise { const deleteFileNode = this.projFileXmlDoc!.createElement(constants.Delete); deleteFileNode.setAttribute(constants.Files, constants.ProjJsonToClean); diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index b37f8fd979..c643d85e2c 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -1577,7 +1577,7 @@ describe('Project: round trip updates', function (): void { await testUpdateInRoundTrip(baselines.SSDTUpdatedProjectBaseline, baselines.SSDTUpdatedProjectAfterSystemDbUpdateBaseline); }); - it('Should update SSDT project to work in ADS handling pre-exsiting targets', async function (): Promise { + it('Should update SSDT project to work in ADS handling pre-existing targets', async function (): Promise { await testUpdateInRoundTrip(baselines.SSDTProjectBaselineWithBeforeBuildTarget, baselines.SSDTProjectBaselineWithBeforeBuildTargetAfterUpdate); }); @@ -1646,3 +1646,55 @@ async function testUpdateInRoundTrip(fileBeforeupdate: string, fileAfterUpdate: should(stub.calledOnce).be.true('showWarningMessage should have been called exactly once'); sinon.restore(); } + +describe('Project: legacy to SDK-style updates', function (): void { + before(async function (): Promise { + await baselines.loadBaselines(); + }); + + beforeEach(function (): void { + sinon.restore(); + }); + + it('Should update legacy style project to SDK-style', async function (): Promise { + const projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const project = await Project.openProject(projFilePath); + + should(project.importedTargets.length).equal(3, 'SSDT and ADS imports should be in the project'); + should(project.isSdkStyleProject).equal(false); + await project.convertProjectToSdkStyle(); + + should(await exists(projFilePath + '_backup')).equal(true, 'Backup file should have been generated before the project was updated'); + should(project.importedTargets.length).equal(0, 'SSDT and ADS imports should have been removed'); + should(project.isSdkStyleProject).equal(true); + }); + + it('Should not update project and no backup file should be created when project is already SDK-style', async function (): Promise { + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline, folderPath); + + const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); + should(project.isSdkStyleProject).equal(true); + await project.convertProjectToSdkStyle(); + + should(await exists(sqlProjPath + '_backup')).equal(false, 'No backup file should have been created'); + should(project.isSdkStyleProject).equal(true); + }); + + it('Should not update project and no backup file should be created when it is an SSDT project that has not been updated to work in ADS', async function (): Promise { + sinon.stub(window, 'showWarningMessage').returns(Promise.resolve(constants.noString)); + // setup test files + const folderPath = await testUtils.generateTestFolderPath(); + const sqlProjPath = await testUtils.createTestSqlProjFile(baselines.SSDTProjectFileBaseline, folderPath); + + const project = await Project.openProject(Uri.file(sqlProjPath).fsPath); + should(project.isSdkStyleProject).equal(false); + should(project.importedTargets.length).equal(2, 'Project should have 2 SSDT imports'); + await project.convertProjectToSdkStyle(); + + should(await exists(sqlProjPath + '_backup')).equal(false, 'No backup file should have been created'); + should(project.importedTargets.length).equal(2, 'Project imports should not have been changed'); + should(project.isSdkStyleProject).equal(false); + }); +});