diff --git a/libs/server/src/lib/utils/utils.ts b/libs/server/src/lib/utils/utils.ts
index 16d57a6cf6..0f5a3a3142 100644
--- a/libs/server/src/lib/utils/utils.ts
+++ b/libs/server/src/lib/utils/utils.ts
@@ -350,6 +350,11 @@ export function toWorkspaceFormat(w: any): WorkspaceJsonConfiguration {
});
});
+ const sortedProjects = Object.entries(w.projects || {}).sort(
+ (projectA, projectB) => projectA[0].localeCompare(projectB[0])
+ );
+ w.projects = Object.fromEntries(sortedProjects);
+
if (w.schematics) {
renameProperty(w, 'schematics', 'generators');
}
diff --git a/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts b/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts
new file mode 100644
index 0000000000..1e24fb9a0c
--- /dev/null
+++ b/libs/vscode/nx-workspace/__mocks__/@nx-console/server.ts
@@ -0,0 +1,22 @@
+const cacheJson = jest.fn();
+
+const fileExistsSync = jest.fn();
+
+const getOutputChannel = jest.fn(() => ({
+ appendLine: jest.fn(),
+ show: jest.fn(),
+}));
+
+const getTelemetry = jest.fn(() => ({
+ exception: jest.fn(),
+}));
+
+const toWorkspaceFormat = jest.fn();
+
+export {
+ cacheJson,
+ fileExistsSync,
+ getOutputChannel,
+ getTelemetry,
+ toWorkspaceFormat,
+};
diff --git a/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts b/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts
new file mode 100644
index 0000000000..538b37efb2
--- /dev/null
+++ b/libs/vscode/nx-workspace/__mocks__/@nx-console/vscode/configuration.ts
@@ -0,0 +1,7 @@
+const WorkspaceConfigurationStore = {
+ instance: {
+ get: jest.fn(),
+ },
+};
+
+export { WorkspaceConfigurationStore };
diff --git a/libs/vscode/nx-workspace/__mocks__/vscode.ts b/libs/vscode/nx-workspace/__mocks__/vscode.ts
new file mode 100644
index 0000000000..5a3ef84cd8
--- /dev/null
+++ b/libs/vscode/nx-workspace/__mocks__/vscode.ts
@@ -0,0 +1,9 @@
+///
+
+const window = {
+ showErrorMessage: jest.fn(),
+};
+
+export = {
+ window,
+};
diff --git a/libs/vscode/nx-workspace/jest.config.js b/libs/vscode/nx-workspace/jest.config.js
index 71049c6a76..3be58c1950 100644
--- a/libs/vscode/nx-workspace/jest.config.js
+++ b/libs/vscode/nx-workspace/jest.config.js
@@ -8,6 +8,9 @@ module.exports = {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
+ moduleNameMapper: {
+ '^@nx-console/vscode/(.+)$': '/libs/vscode/$1/src/index.ts',
+ },
coverageDirectory: '../../../coverage/libs/vscode/nx-workspace',
testEnvironment: 'node',
};
diff --git a/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts b/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts
new file mode 100644
index 0000000000..0680882c97
--- /dev/null
+++ b/libs/vscode/nx-workspace/src/lib/verify-workspace.spec.ts
@@ -0,0 +1,178 @@
+import { verifyWorkspace } from './verify-workspace';
+import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration';
+import * as server from '@nx-console/server';
+import {
+ cacheJson,
+ fileExistsSync,
+ getOutputChannel,
+ getTelemetry,
+} from '@nx-console/server';
+import type { WorkspaceJsonConfiguration } from '@nrwl/devkit';
+import * as vscode from 'vscode';
+
+const mockCacheJsonFn = cacheJson as jest.MockedFunction;
+mockCacheJsonFn.mockImplementation((filePath) => ({
+ json: mockWorkspace,
+ path: filePath,
+}));
+
+const mockStoreInstanceGetFn = WorkspaceConfigurationStore.instance
+ .get as jest.MockedFunction;
+mockStoreInstanceGetFn.mockImplementation(() => workspacePath);
+
+const mockFileExistsSyncFn = fileExistsSync as jest.MockedFunction<
+ typeof fileExistsSync
+>;
+
+const originalNxConsoleServerModule = jest.requireActual('@nx-console/server');
+(server.toWorkspaceFormat as unknown) =
+ originalNxConsoleServerModule.toWorkspaceFormat;
+
+const mockWorkspace: WorkspaceJsonConfiguration = {
+ version: 2,
+ projects: {
+ Project3: {
+ root: 'project-three',
+ },
+ Project1: {
+ root: 'project-one',
+ },
+ Project2: {
+ root: 'project-two',
+ },
+ },
+ defaultProject: undefined,
+ generators: undefined,
+ cli: undefined,
+};
+
+const DefaultWorkspaceInformation = {
+ validWorkspaceJson: false,
+ workspaceType: 'nx',
+ json: {
+ projects: {},
+ version: 2,
+ },
+ configurationFilePath: '',
+};
+
+const workspacePath = './test/fixtures/workspace/';
+
+describe(verifyWorkspace.name, () => {
+ afterEach(() => {
+ mockFileExistsSyncFn.mockClear();
+ });
+
+ describe('when Nx workspace exists', () => {
+ it('returns information about Nx workspace', async () => {
+ // arrange
+ mockFileExistsSyncFn.mockImplementation((filePath) =>
+ /workspace.json$/i.test(filePath)
+ );
+
+ // act
+ const { validWorkspaceJson, json, workspaceType, configurationFilePath } =
+ await verifyWorkspace();
+
+ // assert
+ expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(1);
+ expect(mockFileExistsSyncFn).toHaveLastReturnedWith(true);
+ expect(mockStoreInstanceGetFn).toHaveBeenCalledWith(
+ 'nxWorkspaceJsonPath',
+ ''
+ );
+ expect(mockCacheJsonFn).toHaveBeenCalled();
+ expect(validWorkspaceJson).toBe(true);
+ expect(json).toBeTruthy();
+ expect(json).toEqual(mockWorkspace);
+ expect(workspaceType).toBe('nx');
+ expect(configurationFilePath).toMatch(/workspace.json$/i);
+ });
+ });
+
+ describe('when Ng workspace exists', () => {
+ it('returns information about Ng workspace', async () => {
+ // arrange
+ mockFileExistsSyncFn.mockImplementation((filePath) =>
+ /angular.json$/i.test(filePath)
+ );
+
+ // act
+ const { validWorkspaceJson, json, workspaceType, configurationFilePath } =
+ await verifyWorkspace();
+
+ // assert
+ expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(2);
+ expect(mockFileExistsSyncFn).toHaveNthReturnedWith(1, false);
+ expect(mockFileExistsSyncFn).toHaveNthReturnedWith(2, true);
+ expect(mockStoreInstanceGetFn).toHaveBeenCalledWith(
+ 'nxWorkspaceJsonPath',
+ ''
+ );
+ expect(mockCacheJsonFn).toHaveBeenCalled();
+ expect(validWorkspaceJson).toBe(true);
+ expect(json).toBeTruthy();
+ expect(json).toEqual(mockWorkspace);
+ expect(workspaceType).toBe('ng');
+ expect(configurationFilePath).toMatch(/angular.json$/i);
+ });
+ });
+
+ describe('when workspace json does not exist', () => {
+ it('it shows error dialog and returns default workspace information', async () => {
+ // arrange
+ mockFileExistsSyncFn.mockImplementation(
+ (filePath) => !/(angular|workspace).json$/i.test(filePath)
+ );
+
+ (vscode.window.showErrorMessage as unknown) = jest
+ .fn()
+ .mockResolvedValue('Show Error');
+
+ // act
+ const result = await verifyWorkspace();
+
+ // assert
+ expect(mockFileExistsSyncFn).toHaveBeenCalledTimes(2);
+ expect(mockFileExistsSyncFn).toHaveNthReturnedWith(1, false);
+ expect(mockFileExistsSyncFn).toHaveNthReturnedWith(2, false);
+ expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
+ expect.any(String),
+ 'Show Error'
+ );
+ expect(getOutputChannel).toHaveBeenCalledTimes(3);
+ expect(getTelemetry).toHaveBeenCalledTimes(1);
+ expect(result).toEqual(DefaultWorkspaceInformation);
+ });
+ });
+
+ describe('when workspace information is found', () => {
+ it('projects entries are sorted by entry key', async () => {
+ // arrange
+ mockFileExistsSyncFn.mockImplementationOnce(() => true);
+ const sortedProject = {
+ Project1: {
+ root: 'project-one',
+ },
+ Project2: {
+ root: 'project-two',
+ },
+ Project3: {
+ root: 'project-three',
+ },
+ };
+
+ // act
+ const {
+ json: { projects },
+ } = await verifyWorkspace();
+ const [project1, project2, project3] = Object.keys(projects);
+ const [sorted1, sorted2, sorted3] = Object.keys(sortedProject);
+
+ // assert
+ expect(project1).toBe(sorted1); // should be 'Project1'
+ expect(project2).toBe(sorted2); // should be 'Project2'
+ expect(project3).toBe(sorted3); // should be 'Project3'
+ });
+ });
+});