diff --git a/packages/cds-plugin-ui5/cds-plugin.js b/packages/cds-plugin-ui5/cds-plugin.js
index 803d0486b..7675c3ce4 100644
--- a/packages/cds-plugin-ui5/cds-plugin.js
+++ b/packages/cds-plugin-ui5/cds-plugin.js
@@ -1,147 +1,55 @@
-const path = require("path");
-const fs = require("fs");
-const yaml = require("js-yaml");
-
-const cds = require("@sap/cds");
-const { Router } = require("express");
+// @sap/cds/lib/index.js#138: global.cds = cds // REVISIT: using global.cds seems wrong
+const cds = global.cds || require("@sap/cds"); // reuse already loaded cds!
+const log = require("./lib/log");
+const findUI5Modules = require("./lib/findUI5Modules");
+const createPatchedRouter = require("./lib/createPatchedRouter");
const applyUI5Middleware = require("./lib/applyUI5Middleware");
// marker that the cds-plugin-ui5 plugin is running
// to disable the ui5-middleware-cap if used in apps
process.env["cds-plugin-ui5"] = true;
-/**
- * helper to log colorful messages
- * @param {string} type the type of the message
- * @param {string} message the message text
- */
-function log(type, message) {
- const colors = {
- log: "\x1b[0m", // default
- info: "\x1b[32m", // green
- debug: "\x1b[34m", // blue
- warn: "\x1b[33m", // yellow
- error: "\x1b[31m", // red
- };
- if (!console[type]) {
- type = "log";
- }
- console[type](`\x1b[36m[cds-ui5-plugin]\x1b[0m %s[%s]\x1b[0m %s`, colors[type], type, message);
-}
-
cds.on("bootstrap", async function bootstrap(app) {
- log("debug", "bootstrap");
+ log.debug("bootstrap");
- // lookup the app folder to determine local apps and ui5 app directories
- const localApps = new Set(),
- appDirs = [];
- fs.readdirSync(path.join(process.cwd(), "app"), { withFileTypes: true })
- .filter((f) => f.isDirectory())
- .forEach((d) => localApps.add(d.name));
- localApps.forEach((e) => {
- const d = path.join(process.cwd(), "app", e);
- if (fs.existsSync(path.join(d, "ui5.yaml"))) {
- localApps.delete(e);
- appDirs.push(d);
- }
- });
+ const cwd = process.cwd();
+ const ui5Modules = await findUI5Modules({ cwd });
+ const localApps = ui5Modules.localApps;
- // look for a single app if no apps were found in the app directories
- if(appDirs.length === 0) {
- const d = path.join(process.cwd(), "app");
- if (fs.existsSync(path.join(d, "ui5.yaml"))) {
- appDirs.push(d);
- }
- }
+ const links = [];
- // lookup the UI5 dependencies
- const pkgJson = require(path.join(process.cwd(), "package.json"));
- const deps = [];
- deps.push(...Object.keys(pkgJson.dependencies || {}));
- deps.push(...Object.keys(pkgJson.devDependencies || {}));
- //deps.push(...Object.keys(pkgJson.peerDependencies || {}));
- //deps.push(...Object.keys(pkgJson.optionalDependencies || {}));
- appDirs.push(
- ...deps.filter((dep) => {
- try {
- require.resolve(`${dep}/ui5.yaml`, {
- paths: [process.cwd()],
- });
- return true;
- } catch (e) {
- return false;
- }
- })
- );
+ // register the UI5 modules via their own router/middlewares
+ for await (const ui5Module of ui5Modules) {
+ const { mountPath, modulePath } = ui5Module;
- // if apps are available, attach the middlewares of the UI5 apps
- // to the express of the CAP server via a express router
- if (appDirs) {
- const links = [];
- for await (const appDir of appDirs) {
- // read the ui5.yaml file to extract the configuration
- const ui5YamlPath = require.resolve(path.join(appDir, "ui5.yaml"), {
- paths: [process.cwd()],
- });
- let ui5Configs;
- try {
- const content = fs.readFileSync(ui5YamlPath, "utf-8");
- ui5Configs = yaml.loadAll(content);
- } catch (err) {
- if (err.name === "YAMLException") {
- log("error", `Failed to read ${ui5YamlPath}!`);
- }
- throw err;
- }
+ // mounting the Router for the UI5 application to the CAP server
+ log.info(`Mounting ${mountPath} to UI5 app ${modulePath}`);
- // by default the mount path is derived from the metadata/name
- // and can be overridden by customConfiguration/mountPath
- const ui5Config = ui5Configs?.[0];
- let mountPath = ui5Config?.customConfiguration?.mountPath || ui5Config?.metadata?.name;
- if (!/^\//.test(mountPath)) {
- mountPath = `/${mountPath}`; // always start with /
- }
-
- // mounting the Router for the application to the CAP server
- log("info", `Mounting ${mountPath} to UI5 app ${appDir}`);
- const modulePath = path.dirname(ui5YamlPath);
-
- // create the router and get rid of the mount path
- const router = new Router();
- router.use(function (req, res, next) {
- // disable the compression when livereload is used
- // for loading html-related content (via accept header)
- const accept = req.headers["accept"]?.indexOf("html");
- if (accept && res._livereload) {
- req.headers["accept-encoding"] = "identity";
- }
- // remove the mount path from the url
- req.originalUrl = req.url;
- req.baseUrl = "/";
- next();
- });
+ // create a patched router
+ const router = await createPatchedRouter();
- // apply the UI5 middlewares to the router and
- // retrieve the available HTML pages
- const pages = await applyUI5Middleware(router, {
- basePath: modulePath,
- configPath: modulePath,
- });
+ // apply the UI5 middlewares to the router and
+ // retrieve the available HTML pages
+ const appInfo = await applyUI5Middleware(router, {
+ basePath: modulePath,
+ configPath: modulePath,
+ });
- // append the HTML pages to the links
- pages.forEach((page) => {
- const prefix = mountPath !== "/" ? mountPath : "";
- links.push(`${prefix}${page.getPath()}`);
- });
+ // register the router to the specified mount path
+ app.use(mountPath, router);
- // mount the router to the determined mount path
- app.use(`${mountPath}`, router);
- }
+ // append the HTML pages to the links
+ appInfo.pages.forEach((page) => {
+ const prefix = mountPath !== "/" ? mountPath : "";
+ links.push(`${prefix}${page.getPath()}`);
+ });
+ }
+ if (links.length > 0) {
// register the custom middleware (similar like in @sap/cds/server.js)
app.get("/", function appendLinksToIndex(req, res, next) {
- var send = res.send;
+ const send = res.send;
res.send = function (content) {
// the first
element contains the links to the
// application pages which is fully under control of
@@ -183,11 +91,11 @@ cds.on("bootstrap", async function bootstrap(app) {
ul.innerHTML = newLis.join("\n");
content = doc.toString();
} else {
- log("warn", `Failed to inject application links into CAP index page!`);
+ log.warn(`Failed to inject application links into CAP index page!`);
}
send.apply(this, arguments);
};
- //log("debug", req.url);
+ //log.debug(req.url);
next();
});
@@ -201,10 +109,10 @@ cds.on("bootstrap", async function bootstrap(app) {
if (idxOfServeStatic !== -1) {
middlewareStack.splice(idxOfServeStatic, 0, cmw);
} else {
- log("error", `Failed to determine CAP overview page middleware! You need to manually open the application pages!`);
+ log.error(`Failed to determine CAP overview page middleware! You need to manually open the application pages!`);
}
} else {
- log("error", `Failed to inject application pages to CAP overview page! You need to manually open the application pages!`);
+ log.error(`Failed to inject application pages to CAP overview page! You need to manually open the application pages!`);
}
}
});
@@ -212,6 +120,6 @@ cds.on("bootstrap", async function bootstrap(app) {
// return callback for plugin activation
module.exports = {
activate: function activate(conf) {
- log("debug", "activate", conf);
+ log.debug("activate", conf);
},
};
diff --git a/packages/cds-plugin-ui5/lib/applyUI5Middleware.js b/packages/cds-plugin-ui5/lib/applyUI5Middleware.js
index e8e6846f2..11d96609e 100644
--- a/packages/cds-plugin-ui5/lib/applyUI5Middleware.js
+++ b/packages/cds-plugin-ui5/lib/applyUI5Middleware.js
@@ -1,6 +1,21 @@
const path = require("path");
+/**
+ * @typedef UI5AppInfo
+ * @type {object}
+ * @property {Array} pages root path of the module
+ */
+
// inspired by https://github.com/SAP/karma-ui5/blob/main/lib/framework.js#L466-L522
+/**
+ * Applies the middlewares for the UI5 application located in the given
+ * root directory to the given router.
+ * @param {import("express").Router} router Express Router instance
+ * @param {object} options configuration options
+ * @param {string} options.basePath base path of the UI5 application
+ * @param {string} [options.configPath] path to the ui5.yaml (defaults to "${basePath}/ui5.yaml")
+ * @returns {UI5AppInfo} UI5 application information object
+ */
module.exports = async function applyUI5Middleware(router, { basePath, configPath }) {
const { graphFromPackageDependencies } = await import("@ui5/project/graph");
const { createReaderCollection } = await import("@ui5/fs/resourceFactory");
@@ -54,5 +69,7 @@ module.exports = async function applyUI5Middleware(router, { basePath, configPat
});
await middlewareManager.applyMiddleware(router);
- return await rootReader.byGlob("**/*.html");
+ return {
+ pages: await rootReader.byGlob("**/*.html"),
+ };
};
diff --git a/packages/cds-plugin-ui5/lib/createPatchedRouter.js b/packages/cds-plugin-ui5/lib/createPatchedRouter.js
new file mode 100644
index 000000000..3680efffd
--- /dev/null
+++ b/packages/cds-plugin-ui5/lib/createPatchedRouter.js
@@ -0,0 +1,24 @@
+const { Router } = require("express");
+
+/**
+ * Creates a patched router removing the mount path
+ * from urls and disabling the encoding
+ * @returns {Router} patched router
+ */
+module.exports = async function createPatchedRouter() {
+ // create the router and get rid of the mount path
+ const router = new Router();
+ router.use(function (req, res, next) {
+ // disable the compression when livereload is used
+ // for loading html-related content (via accept header)
+ const accept = req.headers["accept"]?.indexOf("html");
+ if (accept && res._livereload) {
+ req.headers["accept-encoding"] = "identity";
+ }
+ // remove the mount path from the url
+ req.originalUrl = req.url;
+ req.baseUrl = "/";
+ next();
+ });
+ return router;
+};
diff --git a/packages/cds-plugin-ui5/lib/findUI5Modules.js b/packages/cds-plugin-ui5/lib/findUI5Modules.js
new file mode 100644
index 000000000..5c2439289
--- /dev/null
+++ b/packages/cds-plugin-ui5/lib/findUI5Modules.js
@@ -0,0 +1,103 @@
+const path = require("path");
+const fs = require("fs");
+const yaml = require("js-yaml");
+
+const log = require("./log");
+
+/**
+ * @typedef UI5Module
+ * @type {object}
+ * @property {string} modulePath root path of the module
+ * @property {string} mountPath path to mount the module to
+ */
+
+/**
+ * Returns all UI5 modules from local apps and the project dependencies.
+ * @param {object} options configuration options
+ * @param {string} options.cwd current working directory
+ * @returns {Array} array of UI5 module
+ */
+module.exports = async function findApps({ cwd }) {
+ // lookup the app folder to determine local apps and UI5 apps
+ const localApps = new Set();
+ const appDirs = [];
+ const appDir = path.join(cwd, "app");
+ if (fs.existsSync(appDir)) {
+ fs.readdirSync(appDir, { withFileTypes: true })
+ .filter((f) => f.isDirectory())
+ .forEach((d) => localApps.add(d.name));
+ localApps.forEach((e) => {
+ const d = path.join(appDir, e);
+ if (fs.existsSync(path.join(d, "ui5.yaml"))) {
+ localApps.delete(e);
+ appDirs.push(d);
+ }
+ });
+ }
+
+ // look for a single app if no apps were found in the app directories
+ if (appDirs.length === 0) {
+ if (fs.existsSync(path.join(appDir, "ui5.yaml"))) {
+ appDirs.push(appDir);
+ }
+ }
+
+ // lookup the UI5 modules in the project dependencies
+ const pkgJson = require(path.join(cwd, "package.json"));
+ const deps = [];
+ deps.push(...Object.keys(pkgJson.dependencies || {}));
+ deps.push(...Object.keys(pkgJson.devDependencies || {}));
+ //deps.push(...Object.keys(pkgJson.peerDependencies || {}));
+ //deps.push(...Object.keys(pkgJson.optionalDependencies || {}));
+ appDirs.push(
+ ...deps.filter((dep) => {
+ try {
+ require.resolve(`${dep}/ui5.yaml`, {
+ paths: [cwd],
+ });
+ return true;
+ } catch (e) {
+ return false;
+ }
+ })
+ );
+
+ // if apps are available, attach the middlewares of the UI5 apps
+ // to the express of the CAP server via a express router
+ const apps = [];
+ if (appDirs) {
+ for await (const appDir of appDirs) {
+ // read the ui5.yaml file to extract the configuration
+ const ui5YamlPath = require.resolve(path.join(appDir, "ui5.yaml"), {
+ paths: [cwd],
+ });
+ let ui5Configs;
+ try {
+ const content = fs.readFileSync(ui5YamlPath, "utf-8");
+ ui5Configs = yaml.loadAll(content);
+ } catch (err) {
+ if (err.name === "YAMLException") {
+ log("error", `Failed to read ${ui5YamlPath}!`);
+ }
+ throw err;
+ }
+
+ // by default the mount path is derived from the metadata/name
+ // and can be overridden by customConfiguration/mountPath
+ const ui5Config = ui5Configs?.[0];
+ const isApplication = ui5Config?.type === "application";
+ if (isApplication) {
+ let mountPath = ui5Config?.customConfiguration?.mountPath || ui5Config?.metadata?.name;
+ if (!/^\//.test(mountPath)) {
+ mountPath = `/${mountPath}`; // always start with /
+ }
+
+ // determine the module path based on the location of the ui5.yaml
+ const modulePath = path.dirname(ui5YamlPath);
+ apps.push({ modulePath, mountPath });
+ }
+ }
+ }
+ apps.localApps = localApps; // necessary for CAP index.html rewrite
+ return apps;
+};
diff --git a/packages/cds-plugin-ui5/lib/log.js b/packages/cds-plugin-ui5/lib/log.js
new file mode 100644
index 000000000..b47f38bfb
--- /dev/null
+++ b/packages/cds-plugin-ui5/lib/log.js
@@ -0,0 +1,26 @@
+const colors = {
+ log: "\x1b[0m", // default
+ debug: "\x1b[34m", // blue
+ info: "\x1b[32m", // green
+ warn: "\x1b[33m", // yellow
+ error: "\x1b[31m", // red
+};
+
+/**
+ * helper to log colorful messages
+ * @param {string} type the type of the message
+ * @param {...string} message the message text
+ */
+function log(type, ...message) {
+ if (!console[type]) {
+ type = "log";
+ }
+ const args = [`\x1b[36m[cds-ui5-plugin]\x1b[0m %s[%s]\x1b[0m %s`, colors[type], type];
+ args.push(message);
+ console[type].apply(console[type], args);
+}
+
+module.exports = log;
+Object.keys(colors).forEach((level) => {
+ module.exports[level] = log.bind(this, level);
+});