cds-plugin-ui5 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/cds-plugin.js +38 -122
- package/lib/applyUI5Middleware.js +18 -1
- package/lib/createPatchedRouter.js +24 -0
- package/lib/findUI5Modules.js +109 -0
- package/lib/log.js +26 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.1.7](https://github.com/ui5-community/ui5-ecosystem-showcase/compare/cds-plugin-ui5@0.1.6...cds-plugin-ui5@0.1.7) (2023-07-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **cds-plugin-ui5:** allow to select apps or deps ([#775](https://github.com/ui5-community/ui5-ecosystem-showcase/issues/775)) ([aeefb3f](https://github.com/ui5-community/ui5-ecosystem-showcase/commit/aeefb3f5aa70129489f90d684965ade6fc20ef53))
|
|
12
|
+
* **cds-plugin-ui5:** proper dependency handling / decoupling ([#773](https://github.com/ui5-community/ui5-ecosystem-showcase/issues/773)) ([9ef8bf3](https://github.com/ui5-community/ui5-ecosystem-showcase/commit/9ef8bf3da69721a2d599a119407e53c1272fb884))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [0.1.6](https://github.com/ui5-community/ui5-ecosystem-showcase/compare/cds-plugin-ui5@0.1.5...cds-plugin-ui5@0.1.6) (2023-07-11)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* support single app in ./app ([#763](https://github.com/ui5-community/ui5-ecosystem-showcase/issues/763)) ([8642de6](https://github.com/ui5-community/ui5-ecosystem-showcase/commit/8642de6695efd49bc0e1b4e907497a82241bc90d))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
6
29
|
## [0.1.5](https://github.com/ui5-community/ui5-ecosystem-showcase/compare/cds-plugin-ui5@0.1.4...cds-plugin-ui5@0.1.5) (2023-07-01)
|
|
7
30
|
|
|
8
31
|
|
package/cds-plugin.js
CHANGED
|
@@ -1,139 +1,55 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const yaml = require("js-yaml");
|
|
4
|
-
|
|
5
|
-
const cds = require("@sap/cds");
|
|
6
|
-
const { Router } = require("express");
|
|
1
|
+
// @sap/cds/lib/index.js#138: global.cds = cds // REVISIT: using global.cds seems wrong
|
|
2
|
+
const cds = global.cds || require("@sap/cds"); // reuse already loaded cds!
|
|
7
3
|
|
|
4
|
+
const log = require("./lib/log");
|
|
5
|
+
const findUI5Modules = require("./lib/findUI5Modules");
|
|
6
|
+
const createPatchedRouter = require("./lib/createPatchedRouter");
|
|
8
7
|
const applyUI5Middleware = require("./lib/applyUI5Middleware");
|
|
9
8
|
|
|
10
9
|
// marker that the cds-plugin-ui5 plugin is running
|
|
11
10
|
// to disable the ui5-middleware-cap if used in apps
|
|
12
11
|
process.env["cds-plugin-ui5"] = true;
|
|
13
12
|
|
|
14
|
-
/**
|
|
15
|
-
* helper to log colorful messages
|
|
16
|
-
* @param {string} type the type of the message
|
|
17
|
-
* @param {string} message the message text
|
|
18
|
-
*/
|
|
19
|
-
function log(type, message) {
|
|
20
|
-
const colors = {
|
|
21
|
-
log: "\x1b[0m", // default
|
|
22
|
-
info: "\x1b[32m", // green
|
|
23
|
-
debug: "\x1b[34m", // blue
|
|
24
|
-
warn: "\x1b[33m", // yellow
|
|
25
|
-
error: "\x1b[31m", // red
|
|
26
|
-
};
|
|
27
|
-
if (!console[type]) {
|
|
28
|
-
type = "log";
|
|
29
|
-
}
|
|
30
|
-
console[type](`\x1b[36m[cds-ui5-plugin]\x1b[0m %s[%s]\x1b[0m %s`, colors[type], type, message);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
13
|
cds.on("bootstrap", async function bootstrap(app) {
|
|
34
|
-
log("
|
|
14
|
+
log.debug("bootstrap");
|
|
35
15
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
fs.readdirSync(path.join(process.cwd(), "app"), { withFileTypes: true })
|
|
40
|
-
.filter((f) => f.isDirectory())
|
|
41
|
-
.forEach((d) => localApps.add(d.name));
|
|
42
|
-
localApps.forEach((e) => {
|
|
43
|
-
const d = path.join(process.cwd(), "app", e);
|
|
44
|
-
if (fs.existsSync(path.join(d, "ui5.yaml"))) {
|
|
45
|
-
localApps.delete(e);
|
|
46
|
-
appDirs.push(d);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
const ui5Modules = await findUI5Modules({ cwd });
|
|
18
|
+
const localApps = ui5Modules.localApps;
|
|
49
19
|
|
|
50
|
-
|
|
51
|
-
const pkgJson = require(path.join(process.cwd(), "package.json"));
|
|
52
|
-
const deps = [];
|
|
53
|
-
deps.push(...Object.keys(pkgJson.dependencies || {}));
|
|
54
|
-
deps.push(...Object.keys(pkgJson.devDependencies || {}));
|
|
55
|
-
//deps.push(...Object.keys(pkgJson.peerDependencies || {}));
|
|
56
|
-
//deps.push(...Object.keys(pkgJson.optionalDependencies || {}));
|
|
57
|
-
appDirs.push(
|
|
58
|
-
...deps.filter((dep) => {
|
|
59
|
-
try {
|
|
60
|
-
require.resolve(`${dep}/ui5.yaml`, {
|
|
61
|
-
paths: [process.cwd()],
|
|
62
|
-
});
|
|
63
|
-
return true;
|
|
64
|
-
} catch (e) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
// if apps are available, attach the middlewares of the UI5 apps
|
|
71
|
-
// to the express of the CAP server via a express router
|
|
72
|
-
if (appDirs) {
|
|
73
|
-
const links = [];
|
|
74
|
-
for await (const appDir of appDirs) {
|
|
75
|
-
// read the ui5.yaml file to extract the configuration
|
|
76
|
-
const ui5YamlPath = require.resolve(path.join(appDir, "ui5.yaml"), {
|
|
77
|
-
paths: [process.cwd()],
|
|
78
|
-
});
|
|
79
|
-
let ui5Configs;
|
|
80
|
-
try {
|
|
81
|
-
const content = fs.readFileSync(ui5YamlPath, "utf-8");
|
|
82
|
-
ui5Configs = yaml.loadAll(content);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
if (err.name === "YAMLException") {
|
|
85
|
-
log("error", `Failed to read ${ui5YamlPath}!`);
|
|
86
|
-
}
|
|
87
|
-
throw err;
|
|
88
|
-
}
|
|
20
|
+
const links = [];
|
|
89
21
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let mountPath = ui5Config?.customConfiguration?.mountPath || ui5Config?.metadata?.name;
|
|
94
|
-
if (!/^\//.test(mountPath)) {
|
|
95
|
-
mountPath = `/${mountPath}`; // always start with /
|
|
96
|
-
}
|
|
22
|
+
// register the UI5 modules via their own router/middlewares
|
|
23
|
+
for await (const ui5Module of ui5Modules) {
|
|
24
|
+
const { mountPath, modulePath } = ui5Module;
|
|
97
25
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const modulePath = path.dirname(ui5YamlPath);
|
|
26
|
+
// mounting the Router for the UI5 application to the CAP server
|
|
27
|
+
log.info(`Mounting ${mountPath} to UI5 app ${modulePath}`);
|
|
101
28
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
router.use(function (req, res, next) {
|
|
105
|
-
// disable the compression when livereload is used
|
|
106
|
-
// for loading html-related content (via accept header)
|
|
107
|
-
const accept = req.headers["accept"]?.indexOf("html");
|
|
108
|
-
if (accept && res._livereload) {
|
|
109
|
-
req.headers["accept-encoding"] = "identity";
|
|
110
|
-
}
|
|
111
|
-
// remove the mount path from the url
|
|
112
|
-
req.originalUrl = req.url;
|
|
113
|
-
req.baseUrl = "/";
|
|
114
|
-
next();
|
|
115
|
-
});
|
|
29
|
+
// create a patched router
|
|
30
|
+
const router = await createPatchedRouter();
|
|
116
31
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
32
|
+
// apply the UI5 middlewares to the router and
|
|
33
|
+
// retrieve the available HTML pages
|
|
34
|
+
const appInfo = await applyUI5Middleware(router, {
|
|
35
|
+
basePath: modulePath,
|
|
36
|
+
configPath: modulePath,
|
|
37
|
+
});
|
|
123
38
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const prefix = mountPath !== "/" ? mountPath : "";
|
|
127
|
-
links.push(`${prefix}${page.getPath()}`);
|
|
128
|
-
});
|
|
39
|
+
// register the router to the specified mount path
|
|
40
|
+
app.use(mountPath, router);
|
|
129
41
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
42
|
+
// append the HTML pages to the links
|
|
43
|
+
appInfo.pages.forEach((page) => {
|
|
44
|
+
const prefix = mountPath !== "/" ? mountPath : "";
|
|
45
|
+
links.push(`${prefix}${page.getPath()}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
133
48
|
|
|
49
|
+
if (links.length > 0) {
|
|
134
50
|
// register the custom middleware (similar like in @sap/cds/server.js)
|
|
135
51
|
app.get("/", function appendLinksToIndex(req, res, next) {
|
|
136
|
-
|
|
52
|
+
const send = res.send;
|
|
137
53
|
res.send = function (content) {
|
|
138
54
|
// the first <ul> element contains the links to the
|
|
139
55
|
// application pages which is fully under control of
|
|
@@ -175,11 +91,11 @@ cds.on("bootstrap", async function bootstrap(app) {
|
|
|
175
91
|
ul.innerHTML = newLis.join("\n");
|
|
176
92
|
content = doc.toString();
|
|
177
93
|
} else {
|
|
178
|
-
log(
|
|
94
|
+
log.warn(`Failed to inject application links into CAP index page!`);
|
|
179
95
|
}
|
|
180
96
|
send.apply(this, arguments);
|
|
181
97
|
};
|
|
182
|
-
//log(
|
|
98
|
+
//log.debug(req.url);
|
|
183
99
|
next();
|
|
184
100
|
});
|
|
185
101
|
|
|
@@ -193,10 +109,10 @@ cds.on("bootstrap", async function bootstrap(app) {
|
|
|
193
109
|
if (idxOfServeStatic !== -1) {
|
|
194
110
|
middlewareStack.splice(idxOfServeStatic, 0, cmw);
|
|
195
111
|
} else {
|
|
196
|
-
log(
|
|
112
|
+
log.error(`Failed to determine CAP overview page middleware! You need to manually open the application pages!`);
|
|
197
113
|
}
|
|
198
114
|
} else {
|
|
199
|
-
log(
|
|
115
|
+
log.error(`Failed to inject application pages to CAP overview page! You need to manually open the application pages!`);
|
|
200
116
|
}
|
|
201
117
|
}
|
|
202
118
|
});
|
|
@@ -204,6 +120,6 @@ cds.on("bootstrap", async function bootstrap(app) {
|
|
|
204
120
|
// return callback for plugin activation
|
|
205
121
|
module.exports = {
|
|
206
122
|
activate: function activate(conf) {
|
|
207
|
-
log("
|
|
123
|
+
log.debug("activate", conf);
|
|
208
124
|
},
|
|
209
125
|
};
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef UI5AppInfo
|
|
5
|
+
* @type {object}
|
|
6
|
+
* @property {Array<string>} pages root path of the module
|
|
7
|
+
*/
|
|
8
|
+
|
|
3
9
|
// inspired by https://github.com/SAP/karma-ui5/blob/main/lib/framework.js#L466-L522
|
|
10
|
+
/**
|
|
11
|
+
* Applies the middlewares for the UI5 application located in the given
|
|
12
|
+
* root directory to the given router.
|
|
13
|
+
* @param {import("express").Router} router Express Router instance
|
|
14
|
+
* @param {object} options configuration options
|
|
15
|
+
* @param {string} options.basePath base path of the UI5 application
|
|
16
|
+
* @param {string} [options.configPath] path to the ui5.yaml (defaults to "${basePath}/ui5.yaml")
|
|
17
|
+
* @returns {UI5AppInfo} UI5 application information object
|
|
18
|
+
*/
|
|
4
19
|
module.exports = async function applyUI5Middleware(router, { basePath, configPath }) {
|
|
5
20
|
const { graphFromPackageDependencies } = await import("@ui5/project/graph");
|
|
6
21
|
const { createReaderCollection } = await import("@ui5/fs/resourceFactory");
|
|
@@ -54,5 +69,7 @@ module.exports = async function applyUI5Middleware(router, { basePath, configPat
|
|
|
54
69
|
});
|
|
55
70
|
await middlewareManager.applyMiddleware(router);
|
|
56
71
|
|
|
57
|
-
return
|
|
72
|
+
return {
|
|
73
|
+
pages: await rootReader.byGlob("**/*.html"),
|
|
74
|
+
};
|
|
58
75
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { Router } = require("express");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a patched router removing the mount path
|
|
5
|
+
* from urls and disabling the encoding
|
|
6
|
+
* @returns {Router} patched router
|
|
7
|
+
*/
|
|
8
|
+
module.exports = async function createPatchedRouter() {
|
|
9
|
+
// create the router and get rid of the mount path
|
|
10
|
+
const router = new Router();
|
|
11
|
+
router.use(function (req, res, next) {
|
|
12
|
+
// disable the compression when livereload is used
|
|
13
|
+
// for loading html-related content (via accept header)
|
|
14
|
+
const accept = req.headers["accept"]?.indexOf("html");
|
|
15
|
+
if (accept && res._livereload) {
|
|
16
|
+
req.headers["accept-encoding"] = "identity";
|
|
17
|
+
}
|
|
18
|
+
// remove the mount path from the url
|
|
19
|
+
req.originalUrl = req.url;
|
|
20
|
+
req.baseUrl = "/";
|
|
21
|
+
next();
|
|
22
|
+
});
|
|
23
|
+
return router;
|
|
24
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const yaml = require("js-yaml");
|
|
4
|
+
|
|
5
|
+
const log = require("./log");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef UI5Module
|
|
9
|
+
* @type {object}
|
|
10
|
+
* @property {string} modulePath root path of the module
|
|
11
|
+
* @property {string} mountPath path to mount the module to
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns all UI5 modules from local apps and the project dependencies.
|
|
16
|
+
* @param {object} options configuration options
|
|
17
|
+
* @param {string} options.cwd current working directory
|
|
18
|
+
* @param {string} options.skipLocalApps skip local apps
|
|
19
|
+
* @param {string} options.skipDeps skip dependencies
|
|
20
|
+
* @returns {Array<UI5Module>} array of UI5 module
|
|
21
|
+
*/
|
|
22
|
+
module.exports = async function findUI5Modules({ cwd, skipLocalApps, skipDeps }) {
|
|
23
|
+
// lookup the app folder to determine local apps and UI5 apps
|
|
24
|
+
const localApps = new Set();
|
|
25
|
+
const appDirs = [];
|
|
26
|
+
if (!skipLocalApps) {
|
|
27
|
+
const appDir = path.join(cwd, "app");
|
|
28
|
+
if (fs.existsSync(appDir)) {
|
|
29
|
+
fs.readdirSync(appDir, { withFileTypes: true })
|
|
30
|
+
.filter((f) => f.isDirectory())
|
|
31
|
+
.forEach((d) => localApps.add(d.name));
|
|
32
|
+
localApps.forEach((e) => {
|
|
33
|
+
const d = path.join(appDir, e);
|
|
34
|
+
if (fs.existsSync(path.join(d, "ui5.yaml"))) {
|
|
35
|
+
localApps.delete(e);
|
|
36
|
+
appDirs.push(d);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// look for a single app if no apps were found in the app directories
|
|
42
|
+
if (appDirs.length === 0) {
|
|
43
|
+
if (fs.existsSync(path.join(appDir, "ui5.yaml"))) {
|
|
44
|
+
appDirs.push(appDir);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// lookup the UI5 modules in the project dependencies
|
|
50
|
+
if (!skipDeps) {
|
|
51
|
+
const pkgJson = require(path.join(cwd, "package.json"));
|
|
52
|
+
const deps = [];
|
|
53
|
+
deps.push(...Object.keys(pkgJson.dependencies || {}));
|
|
54
|
+
deps.push(...Object.keys(pkgJson.devDependencies || {}));
|
|
55
|
+
//deps.push(...Object.keys(pkgJson.peerDependencies || {}));
|
|
56
|
+
//deps.push(...Object.keys(pkgJson.optionalDependencies || {}));
|
|
57
|
+
appDirs.push(
|
|
58
|
+
...deps.filter((dep) => {
|
|
59
|
+
try {
|
|
60
|
+
require.resolve(`${dep}/ui5.yaml`, {
|
|
61
|
+
paths: [cwd],
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// if apps are available, attach the middlewares of the UI5 apps
|
|
72
|
+
// to the express of the CAP server via a express router
|
|
73
|
+
const apps = [];
|
|
74
|
+
if (appDirs) {
|
|
75
|
+
for await (const appDir of appDirs) {
|
|
76
|
+
// read the ui5.yaml file to extract the configuration
|
|
77
|
+
const ui5YamlPath = require.resolve(path.join(appDir, "ui5.yaml"), {
|
|
78
|
+
paths: [cwd],
|
|
79
|
+
});
|
|
80
|
+
let ui5Configs;
|
|
81
|
+
try {
|
|
82
|
+
const content = fs.readFileSync(ui5YamlPath, "utf-8");
|
|
83
|
+
ui5Configs = yaml.loadAll(content);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.name === "YAMLException") {
|
|
86
|
+
log("error", `Failed to read ${ui5YamlPath}!`);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// by default the mount path is derived from the metadata/name
|
|
92
|
+
// and can be overridden by customConfiguration/mountPath
|
|
93
|
+
const ui5Config = ui5Configs?.[0];
|
|
94
|
+
const isApplication = ui5Config?.type === "application";
|
|
95
|
+
if (isApplication) {
|
|
96
|
+
let mountPath = ui5Config?.customConfiguration?.mountPath || ui5Config?.metadata?.name;
|
|
97
|
+
if (!/^\//.test(mountPath)) {
|
|
98
|
+
mountPath = `/${mountPath}`; // always start with /
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// determine the module path based on the location of the ui5.yaml
|
|
102
|
+
const modulePath = path.dirname(ui5YamlPath);
|
|
103
|
+
apps.push({ modulePath, mountPath });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
apps.localApps = localApps; // necessary for CAP index.html rewrite
|
|
108
|
+
return apps;
|
|
109
|
+
};
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const colors = {
|
|
2
|
+
log: "\x1b[0m", // default
|
|
3
|
+
debug: "\x1b[34m", // blue
|
|
4
|
+
info: "\x1b[32m", // green
|
|
5
|
+
warn: "\x1b[33m", // yellow
|
|
6
|
+
error: "\x1b[31m", // red
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* helper to log colorful messages
|
|
11
|
+
* @param {string} type the type of the message
|
|
12
|
+
* @param {...string} message the message text
|
|
13
|
+
*/
|
|
14
|
+
function log(type, ...message) {
|
|
15
|
+
if (!console[type]) {
|
|
16
|
+
type = "log";
|
|
17
|
+
}
|
|
18
|
+
const args = [`\x1b[36m[cds-ui5-plugin]\x1b[0m %s[%s]\x1b[0m %s`, colors[type], type];
|
|
19
|
+
args.push(message);
|
|
20
|
+
console[type].apply(console[type], args);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = log;
|
|
24
|
+
Object.keys(colors).forEach((level) => {
|
|
25
|
+
module.exports[level] = log.bind(this, level);
|
|
26
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cds-plugin-ui5",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "A CAP server cds-plugin to inject the middlewares of all related UI5 tooling based projects.",
|
|
5
5
|
"author": "Peter Muessig",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,5 +24,5 @@
|
|
|
24
24
|
"@sap/cds": ">=6.8.2",
|
|
25
25
|
"express": ">=4.18.2"
|
|
26
26
|
},
|
|
27
|
-
"gitHead": "
|
|
27
|
+
"gitHead": "03bdac05dfa37252b4df03a865810ab807cb91b5"
|
|
28
28
|
}
|