@sap/cds-dk 8.4.2 → 8.5.0

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/bin/debug.js +90 -0
  3. package/bin/watch.js +12 -2
  4. package/lib/bind/userCdsrcJsonStorageProvider.js +0 -19
  5. package/lib/build/buildTaskEngine.js +1 -1
  6. package/lib/build/provider/hana/2migration.js +34 -20
  7. package/lib/build/provider/hana/index.js +74 -41
  8. package/lib/build/provider/internalBuildTaskProvider.js +1 -1
  9. package/lib/build/provider/mtx-extension/index.js +4 -4
  10. package/lib/build/provider/mtx-sidecar/index.js +2 -2
  11. package/lib/build/provider/nodejs/index.js +3 -19
  12. package/lib/build/util.js +41 -17
  13. package/lib/deploy/to-hana/hdiDeployUtil.js +14 -10
  14. package/lib/import/message.js +2 -1
  15. package/lib/import/odata/metadataconverter/MetadataConverter.js +5 -0
  16. package/lib/import/odata/v4parser.js +32 -43
  17. package/lib/init/add.js +26 -0
  18. package/lib/init/constants.js +4 -5
  19. package/lib/init/index.js +17 -25
  20. package/lib/init/merge.js +1 -4
  21. package/lib/init/projectReader.js +75 -9
  22. package/lib/init/registries/mta.js +0 -1
  23. package/lib/init/template/approuter/files/package.json +1 -1
  24. package/lib/init/template/cloud-logging/files/package.json.hbs +1 -12
  25. package/lib/init/template/cloud-logging/files/values.yaml.hbs +16 -0
  26. package/lib/init/template/cloud-logging/index.js +14 -10
  27. package/lib/init/template/data/as-json.js +1 -1
  28. package/lib/init/template/data/index.js +1 -26
  29. package/lib/init/template/esm/files/package.json.hbs +3 -0
  30. package/lib/init/template/esm/index.js +16 -0
  31. package/lib/init/template/handler/files/handler.xs.hbs +56 -0
  32. package/lib/init/template/handler/index.js +84 -9
  33. package/lib/init/template/helm/index.js +4 -1
  34. package/lib/init/template/http/index.js +102 -35
  35. package/lib/init/template/sample/files/nodejs/{admin-service.js → admin-service.js.hbs} +8 -1
  36. package/lib/init/template/sample/files/nodejs/{cat-service.js → cat-service.js.hbs} +8 -3
  37. package/lib/init/template/sample/index.js +4 -2
  38. package/lib/init/template/telemetry/files/package.json.hbs +18 -0
  39. package/lib/init/template/telemetry/index.js +8 -2
  40. package/lib/init/template/typescript/files/package.json +1 -1
  41. package/lib/init/template/workzone-standard/index.js +1 -1
  42. package/lib/init/types.d.ts +0 -6
  43. package/lib/watch/node-watch.js +25 -9
  44. package/npm-shrinkwrap.json +26 -31
  45. package/package.json +1 -1
package/lib/build/util.js CHANGED
@@ -7,6 +7,7 @@ const IS_WIN = require('os').platform() === 'win32'
7
7
  const { SEVERITY_ERROR, FILE_EXT_CDS } = require('./constants');
8
8
  const DEBUG = cds.debug('cli|build')
9
9
  const SECRETS = /(passw)|(cert)|(ca)|(secret)|(key)/i
10
+ const WS_MAX_DEPTH = 2
10
11
 
11
12
  function getProperty(src, segments) {
12
13
  segments = Array.isArray(segments) ? segments : segments.split('.')
@@ -98,13 +99,13 @@ function _redacted(cred) {
98
99
  if (!cred) return cred
99
100
  if (Array.isArray(cred)) return cred.map(c => typeof c === 'string' ? '...' : _redacted(c))
100
101
  if (typeof cred === 'object') {
101
- const newCred = Object.assign({}, cred)
102
- Object.keys(newCred).forEach(k => (typeof newCred[k] === 'string' && SECRETS.test(k)) ? (newCred[k] = '...') : (newCred[k] = _redacted(newCred[k])))
103
- return newCred
102
+ const newCred = Object.assign({}, cred)
103
+ Object.keys(newCred).forEach(k => (typeof newCred[k] === 'string' && SECRETS.test(k)) ? (newCred[k] = '...') : (newCred[k] = _redacted(newCred[k])))
104
+ return newCred
104
105
  }
105
106
  return cred
106
107
  }
107
-
108
+
108
109
  /**
109
110
  * Returns a list of fully qualified model names belonging to the '@sap' namespace that cannot be resolved.
110
111
  * E.g. the module might NOT have been installed.
@@ -173,28 +174,30 @@ async function pathExists(path) {
173
174
  return fs.promises.access(path).then(() => true).catch(() => false);
174
175
  }
175
176
 
177
+ async function getWorkspacePaths() {
178
+ const wsRoot = findWorkspaceRoot(cds.root)
179
+ if (!wsRoot) {
180
+ return []
181
+ }
182
+ return (await getWorkspaces(wsRoot)).filter(ws => cds.root !== path.join(wsRoot, ws)).map(ws => path.join(wsRoot, ws))
183
+ }
184
+
176
185
  /**
177
186
  * Returns the list of workspaces defined for the current project. A workspace dependency
178
187
  * needs to be defined resulting in a corresponding sym-link.
179
- * @returns the paths are relative to 'cds.root'.
188
+ * @returns the paths are relative to 'wsRoot'.
180
189
  */
181
- async function getWorkspaces(resolve = false, devDependencies = true, wsRoot = cds.root) {
182
- let pgkJsonWsRoot
183
- try {
184
- pgkJsonWsRoot = require(path.join(wsRoot, 'package.json'))
185
- } catch (e) {
186
- if (e.code === 'MODULE_NOT_FOUND') {
187
- DEBUG?.(`No package.json found in project '${wsRoot}', skipping workspace resolution.`)
188
- return []
189
- }
190
- throw e
190
+ async function getWorkspaces(wsRoot, resolve = false, devDependencies = true) {
191
+ wsRoot ??= findWorkspaceRoot(cds.root)
192
+ if (!wsRoot) {
193
+ return []
191
194
  }
192
-
193
- const workspaces = []
195
+ const pgkJsonWsRoot = require(path.join(wsRoot, 'package.json'))
194
196
  let pgkJson = pgkJsonWsRoot
195
197
  if (wsRoot !== cds.root) {
196
198
  pgkJson = require(path.join(cds.root, 'package.json'))
197
199
  }
200
+ const workspaces = []
198
201
  if (pgkJsonWsRoot.workspaces) {
199
202
  if (resolve) {
200
203
  _validateWsDependencies(wsRoot, pgkJson.dependencies, devDependencies ? pgkJson.devDependencies : {})
@@ -215,6 +218,25 @@ async function getWorkspaces(resolve = false, devDependencies = true, wsRoot = c
215
218
  return workspaces
216
219
  }
217
220
 
221
+ function findWorkspaceRoot(currentPath, depth = 0) {
222
+ if (depth > WS_MAX_DEPTH) {
223
+ DEBUG?.(`No workspaces exist, max depth of ${WS_MAX_DEPTH} reached`)
224
+ return
225
+ }
226
+ const packageJson = path.join(currentPath, 'package.json')
227
+ if (fs.existsSync(packageJson)) {
228
+ if (require(path.join(currentPath, 'package.json')).workspaces) {
229
+ return currentPath
230
+ }
231
+ }
232
+ if (fs.existsSync(path.join(currentPath, '.gitmodules'))) {
233
+ return // project root reached
234
+ }
235
+ if (path.dirname(currentPath) !== currentPath) {
236
+ return findWorkspaceRoot(path.dirname(currentPath), depth + 1)
237
+ }
238
+ }
239
+
218
240
  /**
219
241
  * This JavaScript function filterFeaturePaths is used to filter and categorize file paths
220
242
  * from a given model into two categories: base and features.
@@ -452,7 +474,9 @@ module.exports = {
452
474
  getI18nDefaultFolder,
453
475
  normalizePath,
454
476
  pathExists,
477
+ getWorkspacePaths,
455
478
  getWorkspaces,
479
+ findWorkspaceRoot,
456
480
  filterFeaturePaths,
457
481
  getFiles,
458
482
  BuildMessage,
@@ -1,5 +1,4 @@
1
1
  const path = require('path');
2
- const util = require('util');
3
2
  const { bold } = require('../../util/term');
4
3
 
5
4
  const HDI_DEPLOY_LIB = '@sap/hdi-deploy';
@@ -40,15 +39,20 @@ module.exports = new class HdiDeployUtil {
40
39
  if (options.use_hdb === false) options.use_hdb = undefined
41
40
  deployerEnv.HDI_DEPLOY_OPTIONS = JSON.stringify(options)
42
41
 
43
- const asyncDeploy = util.promisify(deploy);
44
- const { exitCode, signal } = await asyncDeploy(dbDir, deployerEnv);
45
- if (exitCode) {
46
- let message = `HDI deployment failed with exit code ${exitCode}`;
47
- if (signal) {
48
- message += `. ${signal}`;
49
- }
50
- throw new Error(message);
51
- }
42
+ return await new Promise((resolve, reject) => {
43
+ deploy(dbDir, deployerEnv, (error, response) => {
44
+ if (error) return reject(error)
45
+ if (response?.exitCode) {
46
+ let message = `HDI deployment failed with exit code ${response.exitCode}.`
47
+ if (response.signal) message += `. ${response.signal}`
48
+ return reject(new Error(message))
49
+ }
50
+ return resolve()
51
+ }, {
52
+ stderrCB: buffer => console.error(buffer.toString()),
53
+ stdoutCB: buffer => console.log(buffer.toString())
54
+ })
55
+ })
52
56
  }
53
57
 
54
58
  _loadHdiDeployLib(cwd) {
@@ -28,7 +28,7 @@ const messages = {
28
28
  BOUND_ACTION_COLLISION: 'The combination of Action name and binding parameter type must be unique for Bound Action ',
29
29
  STREAM_NO_COLLECTION: `'Edm.Stream' cannot be of Collection type.`,
30
30
  UNSUPPORTED_PROPERTY_TYPE: 'Property cannot be of EntityType. Check the type: ',
31
- UNRESOLVED_SCHEMA_NAMESPACE: `Could not resolve the schema `,
31
+ UNRESOLVED_SCHEMA_NAMESPACE: `Could not resolve the schema `,
32
32
  INVALID_DATATYPE: `Invalid Data Type `,
33
33
  ELEMENT_NAME_COLLISION: 'Collision between EntityContainer Element Name and Schema Element Name is currently not supported. Please check the element name: ',
34
34
  NO_DEFAULT_FOR_COLLECTIONS: 'WARNING: Collection type cannot have a Default value.',
@@ -38,6 +38,7 @@ const messages = {
38
38
  NULLABLE_KEY: 'WARNING: Key element cannot be Nullable.',
39
39
  COLLISION_DETECTED: `INFO: Element name collision detected. Adding suffix to affected type category.`,
40
40
  SPECIFY_CONFIG: 'For --config option, please specify correct JSON Object.',
41
+ INVALID_ANNOTATION: 'Annotation element must have a Term attribute',
41
42
  };
42
43
 
43
44
  function getMessages() {
@@ -1,4 +1,6 @@
1
1
  'use strict';
2
+
3
+ const message = require("../../message").getMessages();
2
4
 
3
5
  const vocabulariesMap = new Map([
4
6
  ['org.odata.core.v1', () => require('../vocabularies/Org.OData.Core.V1.json')],
@@ -1754,6 +1756,9 @@ class AnnotationExpression extends Expression {
1754
1756
  annotate(result) { super.annotate(this.getTargetPropertyName(), this.getTarget(), result); }
1755
1757
  interpret() {
1756
1758
  const element = this.getElement();
1759
+ if(!Object.hasOwn(element, "attributes") || !Object.hasOwn(element.attributes, "Term")) {
1760
+ throw new Error(message.INVALID_ANNOTATION);
1761
+ }
1757
1762
  let name = element.attributes.Term;
1758
1763
  if (element.attributes.Qualifier) name += '#' + element.attributes.Qualifier;
1759
1764
  name = '@' + name;
@@ -4,10 +4,10 @@
4
4
  */
5
5
 
6
6
  "use strict";
7
- let messages = require("../message").getMessages();
8
- let common = require("../common");
9
- let versionInfo = require("../../../package.json").version;
10
- let Multimap = require("./util/MultiMap");
7
+ const messages = require("../message").getMessages();
8
+ const common = require("../common");
9
+ const versionInfo = require("../../../package.json").version;
10
+ const Multimap = require("./util/MultiMap");
11
11
  const ignoreObjects = ["$Reference"]; // add all the objects that needs to be ignored for parsing.
12
12
  const { warn, info } = require("../../util/term");
13
13
  const MetadataConverterFactory = require("./metadataconverter/MetadataConverterFactory");
@@ -428,6 +428,11 @@ function _parseAnnotationTerms(annotationTerm, annotationTermObject, csnDefsTarg
428
428
  function _validateAnnotationNamespace(annotationTerm, vocabNamespaceToAlias) {
429
429
  let aliasOrNamespace = annotationTerm.substring(1, annotationTerm.lastIndexOf('.'));
430
430
 
431
+ // This check is for ignoring the @cds annotations given for special cases, e.g. @cds.validate = false for collided action/function
432
+ if (aliasOrNamespace === "cds") {
433
+ return
434
+ }
435
+
431
436
  for (let alias of Object.values(known_vocabularies)) {
432
437
  if (alias === aliasOrNamespace) {
433
438
  return;
@@ -1358,29 +1363,6 @@ function _parseEntityContainer(csnDefs, entityContainerElements, parserContext)
1358
1363
  return csnDefs;
1359
1364
  }
1360
1365
 
1361
- /**
1362
- * @param {List<Object>} dataObjList, action/function object list
1363
- * @returns unique parameter list
1364
- */
1365
- function _collectAllParameters(dataObjList) {
1366
- let uniqueParams = [], paramNames = [], params;
1367
-
1368
- for (let dataObj of dataObjList) {
1369
- params = dataObj.$Parameter;
1370
-
1371
- if (params) {
1372
- for (let i = 0; i < params.length; i++) {
1373
- if (!paramNames.includes(params[i].$Name)) {
1374
- paramNames.push(params[i].$Name);
1375
- uniqueParams.push(params[i]);
1376
- }
1377
- }
1378
- }
1379
- }
1380
-
1381
- return uniqueParams;
1382
- }
1383
-
1384
1366
  /**
1385
1367
  * @param {Boolean} isAction, to identify action/function
1386
1368
  * @param {String} fqName, fully qualified name
@@ -1389,7 +1371,7 @@ function _collectAllParameters(dataObjList) {
1389
1371
  *
1390
1372
  * Scenarios:
1391
1373
  * 1. Unbound Actions can't have collision. Action name must be unique.
1392
- * 2. Unbound Functions can have collision; capture the max number of parameters.
1374
+ * 2. Unbound Functions can have collision; we will parse only one function with the same name having no paramter and a generic String return type.
1393
1375
  */
1394
1376
  function _populateUnboundActionFunction(isAction, fqName, schemaDataList, parserContext) {
1395
1377
  let [schemaName, actFunName] = _extractSchemaNameAndElementName(fqName);
@@ -1409,8 +1391,12 @@ function _populateUnboundActionFunction(isAction, fqName, schemaDataList, parser
1409
1391
  }
1410
1392
  else {
1411
1393
  if (dataList.length > 1) {
1412
- actionFunctionObject['$Parameter'] = _collectAllParameters(dataList);
1394
+ actionFunctionObject['$Parameter'] = [];
1413
1395
  parserContext.unboundFunctions[fqName] = actionFunctionObject;
1396
+ parserContext.unboundFunctions[fqName]["@cds.validate"] = false;
1397
+ if(parserContext.unboundFunctions[fqName].$ReturnType){
1398
+ parserContext.unboundFunctions[fqName].$ReturnType.$Type = "Edm.String";
1399
+ }
1414
1400
  }
1415
1401
  else {
1416
1402
  parserContext.unboundFunctions[fqName] = actionFunctionObject;
@@ -1439,12 +1425,11 @@ function _collectUnboundActionFunction(schemaDataList, parserContext) {
1439
1425
  * @param {Object} parserContext
1440
1426
  *
1441
1427
  * Scenarios for bound actions, functions:
1442
- * 1. Actions with same name and binding parameter type can't have collision.
1443
- * 2. Functions can have collision and we will capture the max number of parameters.
1428
+ * 1. Actions with same name and binding parameter type can have collision and we will parse only one action with that name having no paramter and a genric String return type.
1429
+ * 2. Functions can have collision and we will parse only one function with that name having no paramter and a genric String return type.
1444
1430
  * Special scenario is when same function name is bound to different entity as well.
1445
1431
  */
1446
1432
  function _collectBoundActionFunction(isAction, currNamespace, elementName, elementDataList, parserContext) {
1447
- let functionObjList; // list to collect all the collision obj for function
1448
1433
 
1449
1434
  for (let i = 0; i < elementDataList.length; i++) {
1450
1435
  let dataObj = elementDataList[i];
@@ -1480,7 +1465,12 @@ function _collectBoundActionFunction(isAction, currNamespace, elementName, eleme
1480
1465
  parserContext.entityToBoundActions[boundEntityName][elementName] = dataObj;
1481
1466
  }
1482
1467
  else {
1483
- throw new Error(messages.BOUND_ACTION_COLLISION + `${elementName} in entity ${entityName} of schema ${currNamespace}.`);
1468
+ parserContext.entityToBoundActions[boundEntityName][elementName]["@cds.validate"] = false;
1469
+ parserContext.entityToBoundActions[boundEntityName][elementName]["$Parameter"] = [];
1470
+ if (!parserContext.entityToBoundActions[boundEntityName][elementName].$ReturnType) {
1471
+ parserContext.entityToBoundActions[boundEntityName][elementName].$ReturnType = {};
1472
+ }
1473
+ parserContext.entityToBoundActions[boundEntityName][elementName].$ReturnType.$Type = "Edm.String";
1484
1474
  }
1485
1475
  }
1486
1476
  else {
@@ -1488,18 +1478,12 @@ function _collectBoundActionFunction(isAction, currNamespace, elementName, eleme
1488
1478
  if (!parserContext.entityToBoundFunctions[boundEntityName]) {
1489
1479
  parserContext.entityToBoundFunctions[boundEntityName] = {};
1490
1480
  }
1491
- if (!functionObjList || !parserContext.entityToBoundFunctions[boundEntityName][elementName]) {
1492
- functionObjList = [];
1493
- }
1494
1481
  parserContext.entityToBoundFunctions[boundEntityName][elementName] = dataObj;
1495
- functionObjList.push(dataObj);
1496
1482
  }
1497
1483
  else {
1498
- functionObjList.push(dataObj);
1499
- let functionObject = functionObjList[0];
1500
- functionObject['$Parameter'] = _collectAllParameters(functionObjList);
1501
- functionObjList = [functionObject]; // reset the function object list with recently formed object
1502
- parserContext.entityToBoundFunctions[boundEntityName][elementName] = functionObject;
1484
+ parserContext.entityToBoundFunctions[boundEntityName][elementName]["@cds.validate"] = false;
1485
+ parserContext.entityToBoundFunctions[boundEntityName][elementName]["$Parameter"] = [];
1486
+ parserContext.entityToBoundFunctions[boundEntityName][elementName].$ReturnType.$Type = "Edm.String";
1503
1487
  }
1504
1488
  // throw error if the return type is missing
1505
1489
  if (!parserContext.entityToBoundFunctions[boundEntityName][elementName].$ReturnType) {
@@ -1688,7 +1672,12 @@ function _generateEDMX2JSON(edmx, name_space) {
1688
1672
  if (missingReferencedDocuments?.length > 0) {
1689
1673
  console.log(missingReferencedDocuments);
1690
1674
  }
1691
- resolve(json);
1675
+
1676
+ if (error) {
1677
+ reject(error);
1678
+ } else {
1679
+ resolve(json);
1680
+ }
1692
1681
  });
1693
1682
  } catch (err) {
1694
1683
  reject(err);
package/lib/init/add.js CHANGED
@@ -15,5 +15,31 @@ module.exports = {
15
15
  register: (name, plugin) => {
16
16
  registered.set(name, plugin)
17
17
  },
18
+ filterStringAsRegex,
18
19
  registered
19
20
  }
21
+
22
+ /**
23
+ * Turns a filter string with optional meta characters into a regular expression.
24
+ * If no meta characters are found, the string is treated as an include pattern.
25
+ *
26
+ * @param {string} spec the filter string
27
+ * @returns {RegExp} the regular expression
28
+ */
29
+ function filterStringAsRegex(spec) {
30
+ if (typeof spec === 'string') {
31
+ try {
32
+ if (spec.match(/[\^$|*]/)) {
33
+ spec = spec.replace(/^\*/, '.*') // * at start would be invalid, so make it a .*
34
+ return new RegExp(spec, 'i')
35
+ }
36
+ else { // no meta chars -> include semantics
37
+ spec = spec.replace(/\./g, '\\.') // escape literal dot
38
+ return new RegExp(spec, 'i')
39
+ }
40
+ } catch (err) {
41
+ throw err.message // user error, so cut off stack trace
42
+ }
43
+ }
44
+ return /.*/
45
+ }
@@ -13,9 +13,13 @@ module.exports = {
13
13
  JAVA_MVN: 'java:mvn',
14
14
  JAVA_PACKAGE: 'java:package',
15
15
  NODEJS: 'nodejs',
16
+ ESM: 'esm',
16
17
  SAMPLES: 'samples', // deprecated
17
18
  TINY_SAMPLE: 'tiny-sample',
18
19
  SAMPLE: 'sample',
20
+ TYPER: 'typer',
21
+ TYPESCRIPT: 'typescript',
22
+ HANDLER: 'handler', // influenced by esm, typer and typescript
19
23
  MTA: 'mta',
20
24
  CF_MANIFEST: 'cf-manifest',
21
25
  HELM: 'helm',
@@ -57,8 +61,6 @@ module.exports = {
57
61
  LINT_GLOBAL: 'lint:global',
58
62
  LINT_DEV: 'lint:dev',
59
63
  PIPELINE: 'pipeline',
60
- TYPER: 'typer',
61
- TYPESCRIPT: 'typescript'
62
64
  }),
63
65
 
64
66
  REGEX_JAVA_PACKAGE: /^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/g,
@@ -71,9 +73,6 @@ module.exports = {
71
73
  CAPIRE: 'https://cap.cloud.sap',
72
74
  MAVEN_ARCHETYPE_HELP: `https://cap.cloud.sap/docs/java/developing-applications/building#the-maven-archetype`,
73
75
  MAVEN_INSTALL_HELP: `https://maven.apache.org/install.html`,
74
- CAP_JS_COMMUNITY_ODATA_V2: 'https://github.com/cap-js-community/odata-v2-adapter',
75
- CAP_JS_COMMUNITY_EVENT_QUEUE: 'https://github.com/cap-js-community/event-queue',
76
- CAP_JS_COMMUNITY_WEBSOCKET: 'https://github.com/cap-js-community/websocket',
77
76
  CF_CLI: 'https://github.com/cloudfoundry/cli'
78
77
  }),
79
78
 
package/lib/init/index.js CHANGED
@@ -12,17 +12,13 @@ const DEBUG = /\b(y|all|cli)\b/.test(process.env.DEBUG) ? console.debug : undefi
12
12
 
13
13
  module.exports = class CDSGenerator {
14
14
 
15
- /** @returns {string[]} */
16
- static #findPlugins(excluded) {
17
- // actually needs to be sync to be usable in module.exports of dk/bin/init.js
18
- const fromDk = readdirSync(join(__dirname, 'template'))
19
- const fromPlugins = Object.keys(Object.fromEntries(require('./add').registered))
20
- return [...fromDk, ...fromPlugins]
21
- .filter(plugin => !excluded?.includes(plugin))
15
+ constructor() {
16
+ this.cwd = process.cwd()
17
+ this.uiConfig = require('./bas') // Used by CAP Generator
22
18
  }
23
19
 
24
20
  static help({ exclude = [] } = {}) {
25
- if (!cds.add) return // use case shell completion: cds.add is undefined and not needed
21
+ if (!cds.add) return // shell completion: cds.add is undefined and not needed
26
22
 
27
23
  const plugins = this.readPlugins(exclude)
28
24
  const nameFixedLength = Math.max(...plugins.map(plugin => plugin.name.length))
@@ -35,10 +31,14 @@ module.exports = class CDSGenerator {
35
31
  }
36
32
 
37
33
  /**
34
+ * @param excluded {string[]}
38
35
  * @returns {{name: string, module: import('module')}[]}
39
36
  */
40
37
  static readPlugins(excluded = []) {
41
- const plugins = this.#findPlugins(excluded)
38
+ const fromDk = readdirSync(join(__dirname, 'template'))
39
+ const fromPlugins = Object.keys(Object.fromEntries(require('./add').registered))
40
+ const all = [...fromDk, ...fromPlugins]
41
+ .filter(plugin => !excluded?.includes(plugin))
42
42
  .map(plugin => ({
43
43
  name: plugin,
44
44
  module: cds.add.registered.get(plugin) ?? require('./template/' + plugin)
@@ -51,12 +51,7 @@ module.exports = class CDSGenerator {
51
51
  if (b === -1) return -1
52
52
  return a - b
53
53
  }
54
- return plugins.sort(byPriority)
55
- }
56
-
57
- constructor() {
58
- this.cwd = process.cwd()
59
- this.uiConfig = require('./bas') // Used by CAP Generator
54
+ return all.sort(byPriority)
60
55
  }
61
56
 
62
57
  /**
@@ -216,15 +211,14 @@ module.exports = class CDSGenerator {
216
211
  }
217
212
 
218
213
  const cmds = Object.values(OPTIONS)
219
- this.templateList = Array.from(templates.entries())
220
- .sort(([lhs], [rhs]) => {
221
- const a = cmds.indexOf(lhs), b = cmds.indexOf(rhs)
222
- if (a === -1 && b === -1) return 0
223
- if (a === -1) return 1
224
- if (b === -1) return -1
214
+ const priorities = new Map(cmds.map((cmd, i) => [cmd, i]))
215
+ this.templateList = Array.from(templates.keys())
216
+ .sort((lhs, rhs) => {
217
+ const a = priorities.get(lhs) ?? Infinity
218
+ const b = priorities.get(rhs) ?? Infinity
225
219
  return a - b
226
220
  })
227
- .map((entry) => entry[1])
221
+ .map(key => templates.get(key))
228
222
 
229
223
  DEBUG?.(`Features: ${this.templateList.map(({ name }) => name).join(', ')}`);
230
224
  }
@@ -240,11 +234,9 @@ module.exports = class CDSGenerator {
240
234
 
241
235
  const { options } = cds.cli
242
236
  if (typeof options.add === 'string') {
243
- tokens = options.add.split(/[,\s+]/g);
244
-
237
+ tokens = options.add.split(/[,\s+]/g)
245
238
  } else if (Array.isArray(options.add)) {
246
239
  tokens = options.add
247
-
248
240
  } else if (options.add instanceof Set) {
249
241
  tokens = [...options.add];
250
242
  }
package/lib/init/merge.js CHANGED
@@ -6,11 +6,8 @@ const { readYAML, writeYAML, readJSON } = require('../util/fs')
6
6
  const { detectIndent } = require('./indent')
7
7
  const { colors } = require('../util/term')
8
8
 
9
- function _isObject(item) {
10
- return item && typeof item === 'object' && !Array.isArray(item)
11
- }
12
-
13
9
  function _mergeObject(target, source) {
10
+ const _isObject = item => item && typeof item === 'object' && !Array.isArray(item)
14
11
  const unique = array => [...new Set(array.map(JSON.stringify))].map(JSON.parse)
15
12
  if (_isObject(target) && _isObject(source)) {
16
13
  for (const key in source) {
@@ -1,6 +1,6 @@
1
1
  const cds = require('../cds')
2
2
  const { join, resolve, basename } = require('path')
3
- const { fs, exists, isdir, path } = cds.utils
3
+ const { fs, exists, isfile, isdir, path } = cds.utils
4
4
  const { parseXml } = require('./xml')
5
5
  const { REGEX_JAVA_VERSION, JAVA_LTS_VERSIONS } = require('./constants')
6
6
  const TRACE = cds.debug('trace')
@@ -12,13 +12,15 @@ module.exports = new class ProjectReader {
12
12
  }
13
13
 
14
14
  #setPackageInfo() {
15
+ this.isJava = (exists('pom.xml') || cds.cli.options?.add?.has('java'))
16
+ && !cds.env.for('cds').profiles.includes('node') // opt-out for hybrid projects like sflight
17
+
15
18
  // 1. Use project name and static default values
16
- this.appVersion = cds.cli.options?.add?.has('java') ? '1.0.0-SNAPSHOT' : '1.0.0'
19
+ this.appVersion = this.isJava ? '1.0.0-SNAPSHOT' : '1.0.0'
17
20
  this.appName = this.appId = basename(cds.root)
18
21
  this.appDescription = 'A simple CAP project.'
19
22
 
20
- const isJava = exists('pom.xml') || cds.cli.options?.add?.has('java')
21
- if (isJava) {
23
+ if (this.isJava) {
22
24
  let artifactId, description, version, properties
23
25
  try {
24
26
  ({ artifactId, description, version, properties } = parseXml(resolve(cds.root, 'pom.xml')) ?? {})
@@ -74,6 +76,10 @@ module.exports = new class ProjectReader {
74
76
  * @property {boolean} isUI5 Indicates if the project is a UI5 project.
75
77
  * @property {boolean} isJava Indicates if the project is using Java.
76
78
  * @property {boolean} isNodejs Indicates if the project is using Node.js.
79
+ * @property {boolean} isESM Indicates if the project is an ESM project (type 'module')
80
+ * @property {boolean} hasEsmStyle Indicates if code supports ESM-style imports and exports. This is true for ESM and Typescript projects.
81
+ * @property {boolean} isTypescript Indicates if the project is a Typescript project.
82
+ * @property {boolean} hasTyper Indicates if code supports CDS typer.
77
83
  * @property {boolean} hasHelmUnifiedRuntime Indicates if the project uses Unified Runtime Helm deployment.
78
84
  * @property {boolean} hasHelm Indicates if the project uses CAP Helm deployment.
79
85
  * @property {boolean} hasMta Indicates if the project uses MTA deployment.
@@ -95,7 +101,7 @@ module.exports = new class ProjectReader {
95
101
  * @property {boolean} hasMTXRouteJava Indicates if the approuter should have a multitenant subscription route (java).
96
102
  * @property {boolean} hasMalwareScanner Indicates if the project uses the SAP Malware Scanning service.
97
103
  * @property {boolean} hasDynatrace Indicates if the project uses Dynatrace.
98
- *
104
+ * @property {boolean} hasCloudLogging Indicates if the project uses Cloud Logging.
99
105
  */
100
106
 
101
107
  /**
@@ -110,18 +116,27 @@ module.exports = new class ProjectReader {
110
116
  const { appVersion, appName, appId, appDescription, jdkVersion } = this
111
117
  TRACE?.({ env })
112
118
 
119
+ const pkgJson = () => exists('package.json') ? JSON.parse(fs.readFileSync(join(cds.root, 'package.json'))) : {}
120
+ const _isESM = () => pkgJson().type === 'module'
121
+ const _isTypescript = () => !!pkgJson().dependencies?.typescript || !!pkgJson().devDependencies?.typescript
122
+ const _hasTyper = () => !!pkgJson().dependencies?.['@cap-js/cds-typer'] || !!pkgJson().devDependencies?.['@cap-js/cds-typer']
123
+
113
124
  const _inProd = plugin => require(`./template/${plugin}`).hasInProduction(env)
114
- const _ui5 = () => exists('package.json') && !!JSON.parse(fs.readFileSync(path.resolve(cds.root, 'package.json'))).sapux
125
+ const _ui5 = () => pkgJson().sapux
115
126
  const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json'))
116
127
  const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0
117
- const _isJava = () => exists('pom.xml') || cds.cli.options?.add?.has('java')
128
+ const _isJava = () => this.isJava
118
129
  const isApp = name => name !== 'appconfig' && !name.startsWith('_') && name !== 'router' && name !== 'portal'
119
130
  const reserved = {
120
131
  has: x => _inProd(x),
121
132
  profile: () => cds.cli.options?.for,
122
- language: () => exists('pom.xml') ? 'java' : 'nodejs',
133
+ language: () => _isJava() ? 'java' : 'nodejs',
123
134
  isJava: () => _isJava(),
124
135
  isNodejs: () => !_isJava(),
136
+ isTypescript: () => _isTypescript(),
137
+ isESM: () => _isESM,
138
+ hasEsmStyle: () => _isTypescript() || _isESM(),
139
+ hasTyper: () => _hasTyper(),
125
140
  srvPath: () => join(env.build.target, env.folders.srv.replace(/\/$/, '')).replace(/\\/g, '/'),
126
141
  archiveName: () => {
127
142
  const pomXmlPath = resolve(cds.root, env.folders.srv, 'pom.xml')
@@ -135,7 +150,7 @@ module.exports = new class ProjectReader {
135
150
  const path = join(env.build.target, name).replace(/\\/g, '/')
136
151
  return { folder, name, path }
137
152
  },
138
- configFile: () => exists('pom.xml') ? '.cdsrc.json' : 'package.json',
153
+ configFile: () => _isJava() ? '.cdsrc.json' : 'package.json',
139
154
  apps: () => {
140
155
  const apps = fs.readdirSync(path.resolve(cds.root, cds.env.folders.app)).filter(e =>
141
156
  isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e)
@@ -191,3 +206,54 @@ module.exports = new class ProjectReader {
191
206
  return project
192
207
  }
193
208
  }
209
+
210
+ /**
211
+ * Finds and reads SpringBoot configuration in `cds.root/srv` dir.
212
+ * This is done in a best-effort way. In no way can we replciate all of SpringBoot's
213
+ * config variants.
214
+ * @param {string[]?} roots array of directories to search for application.yaml, like `srv`
215
+ * @returns {Promise<object[]>} array of objects with `cds` property
216
+ */
217
+ module.exports._readSpringBootConfig = async function (roots=[cds.env.folders.srv]) {
218
+ const configNodes = []
219
+ const rootDirs = roots.map(d => join(cds.root, d))
220
+ for (let r of rootDirs) {
221
+ const file = isfile (join (r,'./src/main/resources/application.yaml'))
222
+ if (file) {
223
+ const yaml = cds.load.yaml(file)
224
+ for (let yamlDoc of Array.isArray(yaml) ? yaml : [yaml]) {
225
+ let cds = yamlDoc?.cds;
226
+ if (!cds) continue
227
+ cds = _normalizeSpringBootCfg(cds)
228
+ configNodes.push({cds})
229
+ }
230
+ }
231
+ }
232
+ return configNodes
233
+ }
234
+
235
+ /**
236
+ * Normalize SpringBoot's dots in keys to express nested objects
237
+ *
238
+ * @example: `cds.foo.bar = 1` --> `cds: { foo: { bar: 1 } }`
239
+ * @param {object} obj
240
+ * @returns {object}
241
+ */
242
+ function _normalizeSpringBootCfg(obj) {
243
+ if (typeof obj !== 'object') return obj
244
+ Object.keys(obj).forEach(k => {
245
+ const prop = k.split('.')
246
+ const last = prop.pop()
247
+ // and define the object if not already defined
248
+ const res = prop.reduce((o, key) => {
249
+ // define the object if not defined and return
250
+ return o[key] = o[key] ?? {}
251
+ }, obj)
252
+ res[last] = obj[k]
253
+ // recursively normalize
254
+ _normalizeSpringBootCfg(obj[k])
255
+ // delete the original property from object if it was rewritten
256
+ if (prop.length) delete obj[k]
257
+ })
258
+ return obj
259
+ }
@@ -1,4 +1,3 @@
1
- // Registry of sequence matchers in mta.yaml files
2
1
  const cds = require('../../cds')
3
2
 
4
3
  const srvNode4 = path => ({ in: 'modules', where: { type: 'nodejs', path } })
@@ -4,7 +4,7 @@
4
4
  "@sap/approuter": "^17.0.0"
5
5
  },
6
6
  "engines": {
7
- "node": "^22.0.0"
7
+ "node": "^20"
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node node_modules/@sap/approuter/approuter.js"
@@ -1,18 +1,7 @@
1
1
  {
2
- "dependencies": {
3
- "@cap-js/telemetry": ">=0",
4
- "@grpc/grpc-js": "^1",
5
- "@opentelemetry/exporter-trace-otlp-grpc": "^0",
6
- "@opentelemetry/exporter-metrics-otlp-grpc": "^0",
7
- "@opentelemetry/host-metrics": "^0"
8
- },
9
2
  "cds": {
10
3
  "requires": {
11
- "[{{profile}}]": {
12
- "telemetry": {
13
- "kind": "to-cloud-logging"
14
- }
15
- }
4
+ "cloud-logging": true
16
5
  }
17
6
  }
18
7
  }