diff --git a/extensions/mssql/src/controllers/mainController.ts b/extensions/mssql/src/controllers/mainController.ts index 270fde1692..1d70064c86 100644 --- a/extensions/mssql/src/controllers/mainController.ts +++ b/extensions/mssql/src/controllers/mainController.ts @@ -3085,9 +3085,6 @@ export default class MainController implements vscode.Disposable { * @param projectFilePath The file path of the database project to publish. */ public async onPublishDatabaseProject(projectFilePath: string): Promise { - const deploymentOptionsResult = await this.dacFxService.getDeploymentOptions( - DeploymentScenario.Deployment, - ); const publishProjectWebView = new PublishProjectWebViewController( this._context, this._vscodeWrapper, @@ -3097,7 +3094,6 @@ export default class MainController implements vscode.Disposable { this.sqlProjectsService, this.dacFxService, this.sqlPackageService, - deploymentOptionsResult.defaultDeploymentOptions, ); publishProjectWebView.revealToForeground(); diff --git a/extensions/mssql/src/publishProject/publishProjectWebViewController.ts b/extensions/mssql/src/publishProject/publishProjectWebViewController.ts index de90eae209..4c34627725 100644 --- a/extensions/mssql/src/publishProject/publishProjectWebViewController.ts +++ b/extensions/mssql/src/publishProject/publishProjectWebViewController.ts @@ -37,7 +37,7 @@ import { import { SqlProjectsService } from "../services/sqlProjectsService"; import { Deferred } from "../protocol"; import { TelemetryViews, TelemetryActions } from "../sharedInterfaces/telemetry"; -import { TaskExecutionMode } from "../enums"; +import { TaskExecutionMode, DeploymentScenario } from "../enums"; import { hasAnyMissingRequiredValues, getErrorMessage, uuid } from "../utils/utils"; import { ConnectionCredentials } from "../models/connectionCredentials"; import { ProjectController } from "../controllers/projectController"; @@ -60,6 +60,8 @@ export class PublishProjectWebViewController extends FormWebviewController< > { private _cachedDatabaseList?: { displayName: string; value: string }[]; private _cachedSelectedDatabase?: string; + private _preloadedContainerPort?: Promise; + private _deploymentOptionsPromise?: Promise; private _connectionUri?: string; private _serverTypes: string = ""; private _availableConnections?: IConnectionDialogProfile[]; @@ -81,7 +83,6 @@ export class PublishProjectWebViewController extends FormWebviewController< sqlProjectsService?: SqlProjectsService, dacFxService?: mssql.IDacFxService, sqlPackageService?: SqlPackageService, - deploymentOptions?: mssql.DeploymentOptions, ) { super( context, @@ -101,8 +102,8 @@ export class PublishProjectWebViewController extends FormWebviewController< inProgress: false, lastPublishResult: undefined, hasFormErrors: true, - deploymentOptions: deploymentOptions, - defaultDeploymentOptions: undefined, //Clone after clearing excludeObjectTypes so reset uses the correct defaults + deploymentOptions: undefined, + defaultDeploymentOptions: undefined, } as PublishDialogState, { title: Loc.Title, @@ -122,16 +123,6 @@ export class PublishProjectWebViewController extends FormWebviewController< }, ); - // Clear default excludeObjectTypes for publish dialog, no default exclude options should exist - if (deploymentOptions?.excludeObjectTypes !== undefined) { - deploymentOptions.excludeObjectTypes.value = []; - } - - // Clone after clearing excludeObjectTypes so reset uses the correct defaults - this.state.defaultDeploymentOptions = deploymentOptions - ? structuredClone(deploymentOptions) - : undefined; - this._sqlProjectsService = sqlProjectsService; this._dacFxService = dacFxService; this._sqlPackageService = sqlPackageService; @@ -146,6 +137,7 @@ export class PublishProjectWebViewController extends FormWebviewController< }); this.registerRpcHandlers(); + this.updateState(); // Listen for new connections being added elsewhere (e.g., Object Explorer) // Refresh the saved connections list so new connections appear in the dropdown @@ -162,8 +154,15 @@ export class PublishProjectWebViewController extends FormWebviewController< .then(() => { this.updateState(); this.initialized.resolve(); + // Start port detection after dialog is showing to avoid blocking constructor. + this._preloadedContainerPort = dockerUtils.findAvailablePort( + constants.defaultPortNumber, + ); }) .catch((err) => { + this.logger.error( + `Error initializing PublishProjectWebViewController: ${getErrorMessage(err)}`, + ); this.initialized.reject(err); }); } @@ -207,6 +206,8 @@ export class PublishProjectWebViewController extends FormWebviewController< databaseName: string, upgradeExisting: boolean, ): Promise { + // Ensure deployment options are loaded before executing DacFx operations. + await this._deploymentOptionsPromise; const connectionUri = this._connectionUri || ""; const sqlCmdVariables = new Map(Object.entries(state.formState.sqlCmdVariables || {})); @@ -272,6 +273,8 @@ export class PublishProjectWebViewController extends FormWebviewController< dacpacPath: string, databaseName: string, ): Promise { + // Ensure deployment options are loaded before executing DacFx operations. + await this._deploymentOptionsPromise; const connectionUri = this._connectionUri || ""; const sqlCmdVariables = new Map(Object.entries(state.formState.sqlCmdVariables || {})); @@ -783,6 +786,36 @@ export class PublishProjectWebViewController extends FormWebviewController< this.state.projectFilePath = projectFilePath; } + // Fetch deployment options in the background while other init work proceeds. + // Cache the promise so consumers can await it if they run before the fetch completes. + if (this._dacFxService) { + // getDeploymentOptions returns a Thenable; wrap in Promise.resolve() for .catch support. + this._deploymentOptionsPromise = Promise.resolve( + this._dacFxService.getDeploymentOptions(DeploymentScenario.Deployment), + ) + .then((result) => { + if (this.isDisposed) { + return; + } + const options = result?.defaultDeploymentOptions; + if (options) { + // Clear default excludeObjectTypes — no default exclude options for the publish dialog. + if (options.excludeObjectTypes !== undefined) { + options.excludeObjectTypes.value = []; + } + this.state.deploymentOptions = options; + // Clone after clearing so reset uses the correct defaults. + this.state.defaultDeploymentOptions = structuredClone(options); + this.updateState(); + } + }) + .catch((err) => { + this.logger.error("Failed to fetch deployment options:", err); + }); + // Intentionally not awaited here — callers await _deploymentOptionsPromise before using the options. + void this._deploymentOptionsPromise; + } + // Get the project properties from the proj file let projectTargetVersion: string | undefined; try { @@ -1301,6 +1334,8 @@ export class PublishProjectWebViewController extends FormWebviewController< } try { + // Ensure deployment options are loaded before saving profile. + await this._deploymentOptionsPromise; const databaseName = state.formState.databaseName || projectName; // Connection string depends on publish target: // - For container targets: empty string because we're provisioning a new container @@ -1377,6 +1412,8 @@ export class PublishProjectWebViewController extends FormWebviewController< // Request handler to generate sqlpackage command string this.onRequest(GenerateSqlPackageCommandRequest.type, async (params) => { try { + // Ensure deployment options are loaded before building the command. + await this._deploymentOptionsPromise; const dacpacPath = this.state.projectProperties?.dacpacOutputPath; if (!dacpacPath) { @@ -1628,11 +1665,14 @@ export class PublishProjectWebViewController extends FormWebviewController< this._connectionUri = undefined; this._serverTypes = ""; - // Auto-detect the first port available from 1433 upward so the field - // never shows a port that is already in use. - const availablePort = await dockerUtils.findAvailablePort( - constants.defaultPortNumber, - ); + // Kick off port detection if not already started. + if (!this._preloadedContainerPort) { + this._preloadedContainerPort = dockerUtils.findAvailablePort( + constants.defaultPortNumber, + ); + } + // Use pre-fetched port if available, otherwise fetch live. + const availablePort = await this._preloadedContainerPort; this.state.formState.containerPort = availablePort > 0 ? String(availablePort) : String(constants.defaultPortNumber); } else if (this.state.formState.publishTarget === PublishTarget.ExistingServer) { @@ -1708,7 +1748,12 @@ export class PublishProjectWebViewController extends FormWebviewController< if (String(this.state.formState.containerPort).trim() !== portStr) { return erroredInputs; } - if (availablePort !== portNum) { + // findAvailablePort returns -1 on error (e.g., Docker not running). + // Skip validation in that case to avoid false "port in use" errors. + if (availablePort === -1) { + // Clear any previous validation state when we can't check. + portComponent.validation = { isValid: true, validationMessage: "" }; + } else if (availablePort !== portNum) { portComponent.validation = { isValid: false, validationMessage: Loc.PortAlreadyInUse(portNum), diff --git a/extensions/mssql/test/unit/publishProjectWebViewController.test.ts b/extensions/mssql/test/unit/publishProjectWebViewController.test.ts index 001e434014..4bc75f89d6 100644 --- a/extensions/mssql/test/unit/publishProjectWebViewController.test.ts +++ b/extensions/mssql/test/unit/publishProjectWebViewController.test.ts @@ -90,6 +90,7 @@ suite("PublishProjectWebViewController Tests", () => { // Create mock for interface (IDacFxService) - only stub methods we actually use in tests mockDacFxService = { + getDeploymentOptions: sandbox.stub().resolves({ defaultDeploymentOptions: undefined }), getOptionsFromProfile: sandbox.stub(), savePublishProfile: sandbox.stub(), deployDacpac: sandbox.stub(), @@ -104,6 +105,9 @@ suite("PublishProjectWebViewController Tests", () => { connectionManager: mockConnectionManager, createObjectExplorerSession: sandbox.stub().resolves(), } as unknown as sinon.SinonStubbedInstance; + + // Stub findAvailablePort — called eagerly in the constructor to pre-fetch the port. + sandbox.stub(dockerUtils, "findAvailablePort").resolves(1433); }); teardown(() => { @@ -1517,7 +1521,7 @@ suite("PublishProjectWebViewController Tests", () => { controller.state.formState.containerPort = "1433"; // Simulate port in use: findAvailablePort returns a different port - sandbox.stub(dockerUtils, "findAvailablePort").resolves(1434); + (dockerUtils.findAvailablePort as sinon.SinonStub).resolves(1434); // Stub parent validateForm to return no errors sandbox diff --git a/extensions/sql-database-projects/CHANGELOG.md b/extensions/sql-database-projects/CHANGELOG.md index 56306de795..4e6b1c2a87 100644 --- a/extensions/sql-database-projects/CHANGELOG.md +++ b/extensions/sql-database-projects/CHANGELOG.md @@ -6,6 +6,10 @@ _The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +## [1.5.10] - 2026-06-02 + +- Improved Publish Project dialog performance for faster initial loading, and improved port number validation in Publish Project dialog to correctly show available port. + ## [1.5.9] - 2026-04-22 - Added automatic folder creation (e.g. `dbo/Tables/`) when adding SQL objects to a project. Can be disabled via the `sqlDatabaseProjects.autoCreateFolders` setting.