Skip to content

Commit

Permalink
[FEATURE][PoC] Add UI5 Workspace support
Browse files Browse the repository at this point in the history
Implementing SAP/ui5-tooling#157
  • Loading branch information
RandomByte committed Nov 4, 2022
1 parent 198aa78 commit b3a1b61
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 1 deletion.
129 changes: 129 additions & 0 deletions lib/graph/Workspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import fs from "graceful-fs";
import path from "node:path";
import {promisify} from "node:util";
import logger from "@ui5/logger";

const readFile = promisify(fs.readFile);
const log = logger.getLogger("graph:Workspace");

/**
* Dependency graph node representing a module
*
* @public
* @typedef {object} @ui5/project/graph/Workspace~WorkspaceConfiguration
* @property {string} node.specVersion
* @property {object} node.metadata Version of the project
* @property {object} node.dependencyManagement
*/

/**
* Workspace representation
*
* @public
* @class
* @alias @ui5/project/graph/Workspace
*/
class Workspace {
/**
* @param {object} options
* @param {object} options.cwd
* @param {@ui5/project/graph/Workspace~WorkspaceConfiguration} options.workspaceConfiguration
* Workspace configuration
*/
constructor({cwd, workspaceConfiguration}) {
const {specVersion, metadata, dependencyManagement} = workspaceConfiguration;
if (!cwd || !specVersion || !metadata?.name) {
throw new Error("[Workspace] One or more mandatory parameters not provided");
}

this._name = metadata.name;
this._cwd = cwd;
this._dependencyManagement = dependencyManagement;
}

async getNode(id) {
const nodes = await this._getResolvedNodes();
return nodes.get(id);
}

getNodes() {
return this._getResolvedNodes();
}

_getResolvedNodes() {
if (this._pResolvedNodes) {
return this._pResolvedNodes;
}

return this._pResolvedNodes = this._resolveNodes();
}

async _resolveNodes() {
if (!this._dependencyManagement?.resolutions?.length) {
return [];
}

let resolvedNodes = await Promise.all(this._dependencyManagement.resolutions.map(async (resolutionConfig) => {
if (!resolutionConfig.path) {
throw new Error(
`Missing property 'path' in dependency resolution configuration of workspace ${this._name}`);
}
const nodes = await this._getNodesFromPath(this._cwd, resolutionConfig.path);

if (!Array.isArray(nodes) && resolutionConfig.configuration) {
nodes.configuration = resolutionConfig.configuration;
}
return nodes;
}));

// Flatten array since workspaces might have lead to nested arrays
resolvedNodes = Array.prototype.concat.apply([], resolvedNodes);
return new Map(resolvedNodes.map((node) => {
return [node.id, node];
}));
}

async _getNodesFromPath(cwd, relPath, resolveWorkspace = true) {
const nodePath = path.join(this._cwd, relPath);
const pkg = await this._readPackageJson(nodePath);
if (pkg.workspaces?.length) {
if (!resolveWorkspace) {
log.info(`Ignoring nested package workspace of module ${pkg.name} at ${nodePath}`);
return [];
}
return Promise.all(pkg.workspaces.map(async (workspacePath) => {
const nodes = this._getNodesFromPath(nodePath, workspacePath, false);
if (nodes.lengh > 1) {
throw new Error(
`Package workspace of module ${pkg.name} at ${nodePath} ` +
`unexpectedly resolved to multiple modules`);
}
return nodes[0];
}));
} else {
return this._getNodeFromPackage(pkg, nodePath);
}
}

_getNodeFromPackage(pkg, path) {
return {
id: pkg.name,
version: pkg.version,
path: path
};
}

/**
* Reads the package.json file and returns its content
*
* @private
* @param {string} modulePath Path to the module containing the package.json
* @returns {object} Package json content
*/
async _readPackageJson(modulePath) {
const content = await readFile(path.join(modulePath, "package.json"), "utf8");
return JSON.parse(content);
}
}

export default Workspace;
3 changes: 2 additions & 1 deletion lib/graph/projectGraphBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ function validateNode(node) {
* @function default
* @static
* @param {@ui5/project/graph/ProjectGraphBuilder~NodeProvider} nodeProvider
* @param {@ui5/project/graph/Workspace} workspace
* @returns {@ui5/project/graph/ProjectGraph} A new project graph instance
*/
async function projectGraphBuilder(nodeProvider) {
async function projectGraphBuilder(nodeProvider, workspace) {
const shimCollection = new ShimCollection();
const moduleCollection = {};

Expand Down
69 changes: 69 additions & 0 deletions test/lib/graph/Workspace.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import path from "node:path";
import {fileURLToPath} from "node:url";
import test from "ava";
import sinonGlobal from "sinon";
import esmock from "esmock";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const libraryD = path.join(__dirname, "..", "..", "fixtures", "library.d");
const libraryE = path.join(__dirname, "..", "..", "fixtures", "library.e");

function createWorkspaceConfig({dependencyManagement}) {
return {
specVersion: "2.3",
metadata: {
name: "workspace-name"
},
dependencyManagement
};
}

test.beforeEach(async (t) => {
const sinon = t.context.sinon = sinonGlobal.createSandbox();

t.context.log = {
warn: sinon.stub(),
verbose: sinon.stub(),
error: sinon.stub(),
info: sinon.stub(),
isLevelEnabled: () => true
};

t.context.Workspace = await esmock.p("../../../lib/graph/Workspace.js", {
"@ui5/logger": {
getLogger: sinon.stub().withArgs("graph:Workspace").returns(t.context.log)
}
});
});

test.afterEach.always((t) => {
t.context.sinon.restore();
esmock.purge(t.context.ProjectGraph);
});

test("Basic resolution", async (t) => {
const workspace = new t.context.Workspace({
cwd: __dirname,
workspaceConfiguration: createWorkspaceConfig({
dependencyManagement: {
resolutions: [{
path: "../../fixtures/library.d"
}, {
path: "../../fixtures/library.e"
}]
}
})
});

const nodes = await workspace.getNodes();
t.deepEqual(Array.from(nodes.keys()), ["library.d", "library.e"]);
t.deepEqual(Array.from(nodes.values()), [{
id: "library.d",
path: libraryD,
version: "1.0.0",
}, {
id: "library.e",
path: libraryE,
version: "1.0.0",
}], "Correct node configuration");
});

0 comments on commit b3a1b61

Please sign in to comment.