Skip to content

Commit 0eab288

Browse files
committed
[FEATURE][PoC] Add 'UI5 Workspace' Support
Implementing UI5/cli#157
1 parent def891c commit 0eab288

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

lib/graph/Workspace.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import fs from "graceful-fs";
2+
import path from "node:path";
3+
import {promisify} from "node:util";
4+
import logger from "@ui5/logger";
5+
6+
const readFile = promisify(fs.readFile);
7+
const log = logger.getLogger("graph:Workspace");
8+
9+
/**
10+
* Dependency graph node representing a module
11+
*
12+
* @public
13+
* @typedef {object} @ui5/project/graph/Workspace~WorkspaceConfiguration
14+
* @property {string} node.specVersion
15+
* @property {object} node.metadata Version of the project
16+
* @property {object} node.dependencyManagement
17+
*/
18+
19+
/**
20+
* Workspace representation
21+
*
22+
* @public
23+
* @class
24+
* @alias @ui5/project/graph/Workspace
25+
*/
26+
class Workspace {
27+
/**
28+
* @param {object} options
29+
* @param {object} options.cwd
30+
* @param {@ui5/project/graph/Workspace~WorkspaceConfiguration} options.workspaceConfiguration
31+
* Workspace configuration
32+
*/
33+
constructor({cwd, workspaceConfiguration}) {
34+
if (!cwd || !workspaceConfiguration) {
35+
throw new Error("[Workspace] One or more mandatory parameters not provided");
36+
}
37+
38+
this._cwd = cwd;
39+
this._name = workspaceConfiguration.metadata.name;
40+
this._dependencyManagement = workspaceConfiguration.dependencyManagement;
41+
}
42+
43+
getName() {
44+
return this._name;
45+
}
46+
47+
async getNode(id) {
48+
const nodes = await this._getResolvedNodes();
49+
return nodes.get(id);
50+
}
51+
52+
getNodes() {
53+
return this._getResolvedNodes();
54+
}
55+
56+
_getResolvedNodes() {
57+
if (this._pResolvedNodes) {
58+
return this._pResolvedNodes;
59+
}
60+
61+
return this._pResolvedNodes = this._resolveNodes();
62+
}
63+
64+
async _resolveNodes() {
65+
if (!this._dependencyManagement?.resolutions?.length) {
66+
return new Map();
67+
}
68+
69+
let resolvedNodes = await Promise.all(this._dependencyManagement.resolutions.map(async (resolutionConfig) => {
70+
if (!resolutionConfig.path) {
71+
throw new Error(
72+
`Missing property 'path' in dependency resolution configuration of workspace ${this._name}`);
73+
}
74+
const nodes = await this._getNodesFromPath(this._cwd, resolutionConfig.path);
75+
76+
if (!Array.isArray(nodes) && resolutionConfig.configuration) {
77+
nodes.configuration = resolutionConfig.configuration;
78+
}
79+
return nodes;
80+
}));
81+
82+
// Flatten array since workspaces might have lead to nested arrays
83+
resolvedNodes = Array.prototype.concat.apply([], resolvedNodes);
84+
return new Map(resolvedNodes.map((node) => {
85+
return [node.id, node];
86+
}));
87+
}
88+
89+
async _getNodesFromPath(cwd, relPath, resolveWorkspace = true) {
90+
const nodePath = path.join(this._cwd, relPath);
91+
const pkg = await this._readPackageJson(nodePath);
92+
if (pkg.workspaces?.length) {
93+
if (!resolveWorkspace) {
94+
log.info(`Ignoring nested package workspace of module ${pkg.name} at ${nodePath}`);
95+
return [];
96+
}
97+
return Promise.all(pkg.workspaces.map(async (workspacePath) => {
98+
const nodes = await this._getNodesFromPath(nodePath, workspacePath, false);
99+
if (nodes.lengh > 1) {
100+
throw new Error(
101+
`Package workspace of module ${pkg.name} at ${nodePath} ` +
102+
`unexpectedly resolved to multiple modules`);
103+
}
104+
return nodes[0];
105+
}));
106+
} else {
107+
return this._getNodeFromPackage(pkg, nodePath);
108+
}
109+
}
110+
111+
_getNodeFromPackage(pkg, path) {
112+
return {
113+
id: pkg.name,
114+
version: pkg.version,
115+
path: path
116+
};
117+
}
118+
119+
/**
120+
* Reads the package.json file and returns its content
121+
*
122+
* @private
123+
* @param {string} modulePath Path to the module containing the package.json
124+
* @returns {object} Package json content
125+
*/
126+
async _readPackageJson(modulePath) {
127+
const content = await readFile(path.join(modulePath, "package.json"), "utf8");
128+
return JSON.parse(content);
129+
}
130+
}
131+
132+
export default Workspace;

test/lib/graph/Workspace.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import path from "node:path";
2+
import {fileURLToPath} from "node:url";
3+
import test from "ava";
4+
import sinonGlobal from "sinon";
5+
import esmock from "esmock";
6+
7+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
8+
const libraryD = path.join(__dirname, "..", "..", "fixtures", "library.d");
9+
const libraryE = path.join(__dirname, "..", "..", "fixtures", "library.e");
10+
11+
function createWorkspaceConfig({dependencyManagement}) {
12+
return {
13+
specVersion: "2.3",
14+
metadata: {
15+
name: "workspace-name"
16+
},
17+
dependencyManagement
18+
};
19+
}
20+
21+
test.beforeEach(async (t) => {
22+
const sinon = t.context.sinon = sinonGlobal.createSandbox();
23+
24+
t.context.log = {
25+
warn: sinon.stub(),
26+
verbose: sinon.stub(),
27+
error: sinon.stub(),
28+
info: sinon.stub(),
29+
isLevelEnabled: () => true
30+
};
31+
32+
t.context.Workspace = await esmock.p("../../../lib/graph/Workspace.js", {
33+
"@ui5/logger": {
34+
getLogger: sinon.stub().withArgs("graph:Workspace").returns(t.context.log)
35+
}
36+
});
37+
});
38+
39+
test.afterEach.always((t) => {
40+
t.context.sinon.restore();
41+
esmock.purge(t.context.ProjectGraph);
42+
});
43+
44+
test("Basic resolution", async (t) => {
45+
const workspace = new t.context.Workspace({
46+
cwd: __dirname,
47+
workspaceConfiguration: createWorkspaceConfig({
48+
dependencyManagement: {
49+
resolutions: [{
50+
path: "../../fixtures/library.d"
51+
}, {
52+
path: "../../fixtures/library.e"
53+
}]
54+
}
55+
})
56+
});
57+
58+
const nodes = await workspace.getNodes();
59+
t.deepEqual(Array.from(nodes.keys()), ["library.d", "library.e"], "Correct node keys");
60+
t.deepEqual(Array.from(nodes.values()), [{
61+
id: "library.d",
62+
path: libraryD,
63+
version: "1.0.0",
64+
}, {
65+
id: "library.e",
66+
path: libraryE,
67+
version: "1.0.0",
68+
}], "Correct node configuration");
69+
});

0 commit comments

Comments
 (0)