@sap/cds-dk 9.4.1 → 9.4.3

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 (75) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/data-products/index.js +10 -2
  3. package/bin/data-products/prepare-csn.js +63 -26
  4. package/bin/help.js +2 -2
  5. package/bin/import.js +7 -2
  6. package/bin/up.js +25 -4
  7. package/bin/util/containerize.js +142 -0
  8. package/lib/build/buildTaskEngine.js +5 -2
  9. package/lib/build/plugin.js +1 -1
  10. package/lib/build/plugins/helm/index.js +6 -7
  11. package/lib/build/provider/hana/index.js +10 -2
  12. package/lib/import/common.js +3 -3
  13. package/lib/import/message.js +1 -1
  14. package/lib/init/constants.js +3 -1
  15. package/lib/init/index.js +4 -5
  16. package/lib/init/projectReader.js +101 -77
  17. package/lib/init/registries/mta.js +3 -0
  18. package/lib/init/registries/xs-app.js +7 -1
  19. package/lib/init/template/ams/index.js +7 -6
  20. package/lib/init/template/app-frontend/files/mta.yaml.hbs +0 -7
  21. package/lib/init/template/app-frontend/index.js +2 -2
  22. package/lib/init/template/application-logging/index.js +13 -10
  23. package/lib/init/template/approuter/files/mta.yaml.hbs +7 -0
  24. package/lib/init/template/approuter/index.js +30 -18
  25. package/lib/init/template/attachments/index.js +3 -3
  26. package/lib/init/template/audit-logging/index.js +4 -4
  27. package/lib/init/template/cloud-logging/index.js +13 -10
  28. package/lib/init/template/connectivity/index.js +3 -3
  29. package/lib/init/template/containerize/files/containerize.yaml.hbs +1 -2
  30. package/lib/init/template/destination/index.js +5 -9
  31. package/lib/init/template/dynatrace/index.js +13 -10
  32. package/lib/init/template/enterprise-messaging/index.js +5 -5
  33. package/lib/init/template/enterprise-messaging-shared/index.js +4 -4
  34. package/lib/init/template/extensibility/index.js +2 -2
  35. package/lib/init/template/file-based-messaging/index.js +2 -2
  36. package/lib/init/template/github-actions/files/kyma-setup/action.yaml +0 -2
  37. package/lib/init/template/github-actions/index.js +3 -3
  38. package/lib/init/template/hana/index.js +19 -22
  39. package/lib/init/template/helm/index.js +47 -50
  40. package/lib/init/template/html5-repo/files/containerize.yaml.hbs +0 -10
  41. package/lib/init/template/html5-repo/files/mta.yaml.hbs +3 -7
  42. package/lib/init/template/html5-repo/files/values.yaml.hbs +6 -0
  43. package/lib/init/template/html5-repo/files/xs-app.json.hbs +5 -3
  44. package/lib/init/template/html5-repo/index.js +10 -8
  45. package/lib/init/template/http/index.js +31 -10
  46. package/lib/init/template/ias/files/mta.yaml.hbs +3 -0
  47. package/lib/init/template/ias/files/values.yaml.hbs +4 -1
  48. package/lib/init/template/ias/index.js +10 -11
  49. package/lib/init/template/kafka/index.js +3 -3
  50. package/lib/init/template/local-messaging/index.js +2 -2
  51. package/lib/init/template/malware-scanner/index.js +3 -7
  52. package/lib/init/template/mta/files/mta.yaml.hbs +10 -0
  53. package/lib/init/template/multitenancy/index.js +8 -10
  54. package/lib/init/template/notifications/index.js +2 -2
  55. package/lib/init/template/pipeline/index.js +5 -1
  56. package/lib/init/template/portal/files/mta.yaml.hbs +6 -0
  57. package/lib/init/template/portal/index.js +7 -7
  58. package/lib/init/template/postgres/index.js +4 -4
  59. package/lib/init/template/redis/index.js +2 -2
  60. package/lib/init/template/redis-messaging/index.js +2 -2
  61. package/lib/init/template/sample/files/app/admin-books/fiori-service.cds +2 -2
  62. package/lib/init/template/sample/files/javascript/admin-service.js.hbs +24 -0
  63. package/lib/init/template/sample/files/javascript/cat-service.js.hbs +43 -0
  64. package/lib/init/template/sample/files/typescript/admin-service.ts.hbs +16 -0
  65. package/lib/init/template/sample/files/typescript/cat-service.ts.hbs +31 -0
  66. package/lib/init/template/test/files/test/test.xs.hbs +1 -1
  67. package/lib/init/template/test/index.js +2 -2
  68. package/lib/init/template/tiny-sample/index.js +1 -1
  69. package/lib/init/template/workzone-standard/index.js +5 -6
  70. package/lib/init/template/xsuaa/files/mta.yaml.hbs +0 -2
  71. package/lib/init/template/xsuaa/index.js +24 -19
  72. package/lib/lint/eslintHelp.json +1 -1
  73. package/lib/util/question.js +1 -1
  74. package/npm-shrinkwrap.json +22 -22
  75. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
8
8
 
9
+ ## Version 9.4.3 - 2025-10-29
10
+
11
+ ### Changed
12
+
13
+ - `cds add containerize` omits the Unix-based `before_all` scripts in `containerize.yaml` in favor of a cross-platform compatible `cds up --to k8s` integration.
14
+ - `cds up --to k8s` now uses built-in containerization, eliminating the hard dependency on the `ctz` library.
15
+ - `cds up --to k8s` will interactively ask for a domain if it can't be determined from the current Kubernetes configuration.
16
+ - `cds build --for hana` no longer requires `.hdbgrants` and `.hdbrevokes` to be specified as a plugin in the `.hdiconfig` file.
17
+
18
+ ### Fixed
19
+
20
+ - `cds add pipeline` does not add UI5 template files in some cases.
21
+ - `cds add ias` does not write to the `mta.yaml` for non-MTA projects.
22
+ - `cds add ias` automatically adds the `authenticationType` to `xs-app.json` if required.
23
+ - `cds add ias` adds a binding to the approuter component if required.
24
+ - `cds import --from ...` yields a better error message for unknown import kinds.
25
+
26
+ ## Version 9.4.2 - 2025-10-23
27
+
28
+ ### Changed
29
+
30
+ - `cds add containerize` does not add app module installations scripts to its `containerize.yaml`. Use `cds up --to k8s` instead.
31
+ - `cds add` is less eager with respect to changing unaffected configuration.
32
+ - `cds add ias` adds less unnecessary configuration in combination with the HTML5 repository.
33
+ - `cds add test` includes the service name in the test suite description.
34
+
35
+ ### Fixed
36
+
37
+ - `cds add ams` followed by `cds add mta` correctly generates the AMS deployer module.
38
+ - `cds add hana` does not fail if there's no `.cdsrc.json`.
39
+ - `cds up` doesn't try to create a symlink to the monorepo _package-lock.json_ if a submodule-local one already exists.
40
+ - `cds add test` creates correct OData URLs with unqualified entity names in service paths.
41
+ - `cds add http` skips contained composition targets.
42
+ - `cds add sample` no longer comes with an error when creating a Book through Fiori.
43
+
9
44
  ## Version 9.4.1 - 2025-10-09
10
45
 
11
46
  ### Added
@@ -6,8 +6,16 @@ const path = require ('node:path')
6
6
 
7
7
  exports.load_data_product = async function (sources, o) {
8
8
  let service = /\/([^/]+)\//.exec(o.services)?.[1].replaceAll('\\.','.')
9
- if (!service.includes(':apiResource:')) {
10
- const [,namespace,name,version] = /^(.+)-([^-]+)-(v[0-9]+)\.json$/ .exec (path.basename(String(sources)))
9
+ if (service != 'all') {
10
+ // ORD ID has been provided as parameter "-s", valid example: "sap.s4com:apiResource:Product:v1"
11
+ if (!service.match(/^([^:]+):apiResource:([^:]+):(v[0-9]+)$/))
12
+ throw new Error(`"${service}" is not a valid output port ORD ID`)
13
+ }
14
+ else {
15
+ // try to deduce ORD ID from file name, valid example: "sap-s4com-Product-v1.json"
16
+ const m = /^(.+)-([^-]+)-(v[0-9]+)\.(json|csn)$/ .exec (path.basename(String(sources)))
17
+ if (m?.length != 5) throw new Error(`Output port ORD ID cannot be deduced from file name`)
18
+ const [,namespace,name,version] = m
11
19
  service = namespace.replace(/-/g,'.') + ':apiResource:' + name + ':' + version
12
20
  }
13
21
 
@@ -180,38 +180,75 @@ exports.prepareCsn = function (csn, srvName, ordId) {
180
180
  //
181
181
  // ---------- step 5: wrap everyting into service if not yet done -------------------------------
182
182
  //
183
- // Prefix all entity names with service name. Adapt association targets.
184
- // Add CAP specific annotations for DP handling.
183
+ // This function must be called with a service name "srvName" as argument.
184
+ if (!srvName) throw new Error('No service name has been provided.')
185
185
 
186
- if (srvName) {
187
- if (!csn.definitions[srvName] || csn.definitions[srvName].kind !== 'service') {
188
- // add the service
189
- csn.definitions[srvName] = {kind: 'service'}
190
- // loop over all entities and prefix names with service name
191
- for (let n in csn.definitions) {
192
- let entity = csn.definitions[n]
193
- if (entity.kind === 'entity') {
194
- // find association elements and rename the targets
195
- for (let en in entity.elements || {} ) {
196
- let element = entity.elements[en]
197
- if (element?.type == 'cds.Association' || element?.type == 'cds.Composition') {
198
- element.target = srvName + '.' + element.target
199
- }
200
- }
201
- csn.definitions[srvName + '.' + n] = entity
202
- delete csn.definitions[n]
186
+ // Assumptions (based on the rules for DP CSNs):
187
+ // 1) there is no or one context or service in the CSN
188
+ // 2) if there is a context or service, all objects in the CSN belong to it
189
+ //
190
+ // If there is no context/service (this is a DP CSN coming from BAH):
191
+ // put all entities into a new service "srvName", i.e.
192
+ // - prefix all object and association target names with "srvName"
193
+ // - add a service "srvName" with CAP specific annotations for DP handling
194
+ // If there is a context/service (DP CSN comes from UCL, context/service name is the schema name in the HDLFS share):
195
+ // move all entities out of the context/service and put them into a new service "srvName", i.e.
196
+ // - remove the context/service
197
+ // - change the prefix for all entity and association target names from
198
+ // the old context/service name to "srvName"
199
+ // - add a service "srvName" with CAP specific annotations for DP handling
200
+ //
201
+ // Note: Currently an existing context/service doesn't contain further info (e.g. annos),
202
+ // so deleting it is ok.
203
+
204
+ let ctxName = null;
205
+ const ctxs = Object.entries(csn.definitions).filter(([,o]) => o.kind === 'context' || o.kind === 'service')
206
+ if (ctxs.length > 0) {
207
+ // check assumption 1
208
+ if (ctxs.length > 1) throw new Error('Data Product CSN is invalid: it has more than one context or service.')
209
+ ctxName = ctxs[0][0]
210
+ delete csn.definitions[ctxName]
211
+ // check assumption 2
212
+ const bad = Object.keys(csn.definitions).filter((n) => !n.startsWith(ctxName + '.'))
213
+ if (bad.length > 0)
214
+ throw new Error(`Unexpected entity name "${bad[0]}" without prefix in Data product CSN with context "${ctxName}"`)
215
+ }
216
+
217
+ function changePrefix(name) {
218
+ if (ctxName) {
219
+ name = name.substring(ctxName.length + 1) // remove prefix incl. '.'
220
+ }
221
+ return srvName + '.' + name
222
+ }
223
+
224
+ // loop over all definitions and prefix names with service name
225
+ // if ctxName accidentially equals srvName, do nothing
226
+ if (ctxName != srvName) {
227
+ for (let n in csn.definitions) {
228
+ let def = csn.definitions[n]
229
+ // adapt assoc targets
230
+ for (let en in def.elements || {} ) {
231
+ let element = def.elements[en]
232
+ if (element?.type === 'cds.Association' || element?.type === 'cds.Composition') {
233
+ element.target = changePrefix(element.target)
203
234
  }
204
235
  }
236
+ csn.definitions[changePrefix(n)] = def
237
+ delete csn.definitions[n]
205
238
  }
206
- // add service annotations
207
- Object.assign(csn.definitions[srvName], {
208
- '@cds.dp.ordId': ordId,
209
- '@cds.external': true,
210
- '@data.product': true,
211
- '@protocol' : 'none' // don't serve via OData
212
- })
213
239
  }
214
240
 
241
+ // add the service
242
+ csn.definitions[srvName] = {
243
+ kind : 'service',
244
+ '@cds.dp.ordId': ordId,
245
+ '@cds.external': true,
246
+ '@data.product': true,
247
+ '@protocol' : 'none' // don't serve via OData
248
+ };
249
+ if (ctxName) csn.definitions[srvName]['@data.product.schema'] = ctxName;
250
+
251
+
215
252
  //
216
253
  // ---------- step 6: move entity/element annotations into extensions section -------------------
217
254
  //
package/bin/help.js CHANGED
@@ -32,10 +32,10 @@ module.exports = exports = Object.assign (help, {formatHelp, handleCompletion, h
32
32
  * * | *logout* logout from extensible multitenant SaaS app
33
33
  * * | *pull* pull base model of extensible SaaS app
34
34
  * * | *push* push extension to extensible SaaS app
35
- *t* | *lint* run linter for env or model checks
35
+ * * | *lint* run linter for env or model checks
36
36
  *v* | *version* get detailed version information
37
37
  * * | *completion* add/remove cli completion for cds commands
38
- *?* | *help* get detailed usage information
38
+ *h* | *help* get detailed usage information
39
39
 
40
40
  Learn more about each command using:
41
41
  *cds help* <command> or
package/bin/import.js CHANGED
@@ -1,6 +1,6 @@
1
1
  module.exports = Object.assign(_import, {
2
2
  _name: 'import',
3
- options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group' ],
3
+ options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group', '--data-product' ],
4
4
  shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d', undefined, '-c', undefined, undefined, '-', '-f'],
5
5
  flags: [ '--dry', '--force', '--no-copy', '--no-save', '--keep-namespace', '--data-product', '--edmx', '--rfc', '--odata', '--openapi', '--asyncapi' ],
6
6
  help: `
@@ -55,6 +55,11 @@ module.exports = Object.assign(_import, {
55
55
  Destination name under which the remote service can be reached.
56
56
  Not supported by import types "edmx", "openapi" or "asyncapi".
57
57
 
58
+ *--data-product*
59
+ Tells cds import to import a data product. The source must be a valid
60
+ data product CSN file, e.g. as found on http://api.sap.com/dataproducts.
61
+ (Not released with general availability yet)
62
+
58
63
  *Below options are valid for OData only*
59
64
 
60
65
  *--keep-namespace*
@@ -153,7 +158,7 @@ async function _import(args, options = {}) {
153
158
  if (options.from) {
154
159
  if (!cds.import.from[options.from]) {
155
160
  // the required plugin might not be installed, e.g. rfc importer
156
- throw messages.UNSUPPORTED_FROM_OPTION;
161
+ throw messages.UNSUPPORTED_FROM_OPTION.replace('{0}', options.from).replace('{1}', Object.keys(cds.import.from).join(', '));
157
162
  }
158
163
  csn = await cds.import.from[options.from](input, apiOptions);
159
164
  } else {
package/bin/up.js CHANGED
@@ -32,6 +32,8 @@ const cds = require('../lib/cds')
32
32
  const { highlight } = require('../lib/util/term')
33
33
  const { exists, fs, path, path: { join } } = cds.utils
34
34
  const { readProject } = require('../lib/init/projectReader')
35
+ const { mkdirp, copy } = cds.utils
36
+ const { containerize } = require('../bin/util/containerize')
35
37
 
36
38
  const run = cmd => {
37
39
  console.log(highlight(cmd))
@@ -49,10 +51,16 @@ async function up() {
49
51
  const project = readProject()
50
52
  const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice } = project
51
53
 
54
+ const html5DeployerPath = join(cds.root, cds.env.folders.app, 'html5-deployer', 'resources')
55
+
52
56
  for (const { app } of apps) {
53
57
  const appPath = path.join(cds.root, cds.env.folders.app, app)
54
- if (exists(path.join(appPath, 'package.json')) && !exists(path.join(appPath, 'package-lock.json'))) {
55
- run(`npm i --package-lock-only --prefix ${appPath}`)
58
+ if (exists(path.join(appPath, 'package.json'))) {
59
+ if (!exists(path.join(appPath, 'package-lock.json'))) {
60
+ run(`npm i --prefix ${appPath}`)
61
+ } else if (process.env.CI) {
62
+ run(`npm ci --prefix ${appPath}`)
63
+ }
56
64
  }
57
65
  }
58
66
 
@@ -75,6 +83,16 @@ async function up() {
75
83
 
76
84
  if (cds.cli.options.to === 'k8s') {
77
85
  if (!exists('chart')) run('cds add helm')
86
+ if (apps.length > 0) {
87
+ await mkdirp(html5DeployerPath)
88
+ }
89
+
90
+ for (const { app } of apps) {
91
+ const appPath = path.join(cds.root, cds.env.folders.app, app)
92
+ run(`npm run build --prefix ${appPath}`)
93
+ await copy(path.join(appPath, `dist/${app}.zip`)).to(html5DeployerPath, `${app}.zip`)
94
+ }
95
+
78
96
  if (!exists('containerize.yaml')) run('cds add containerize')
79
97
  } else {
80
98
  if (!exists('mta.yaml')) {
@@ -91,7 +109,7 @@ async function up() {
91
109
  const target = path.join(process.cwd(), 'package-lock.json')
92
110
  if (exists(target) && fs.lstatSync(target).isSymbolicLink()) fs.unlinkSync(target)
93
111
  const relative = path.relative(process.cwd(), source)
94
- fs.symlinkSync(relative, target, 'junction')
112
+ if (!exists(target)) fs.symlinkSync(relative, target, 'junction')
95
113
  }
96
114
 
97
115
  if (cds.cli.options.to === 'k8s') {
@@ -100,7 +118,10 @@ async function up() {
100
118
  const nsKubectl = ns ? `-n ${ns}` : ''
101
119
 
102
120
  run('npm i --package-lock')
103
- run('ctz containerize.yaml --log --push')
121
+ run('cds build --production')
122
+ await containerize('containerize.yaml', { push: true, clearCache: !!process.env.CLEAR_CACHE })
123
+ console.log('Containerization completed')
124
+
104
125
  let helmUpgrade = `helm upgrade --install ${appName} ./gen/chart ${nsHelm} --wait --wait-for-jobs --timeout=10m`
105
126
  if (hasXsuaa) helmUpgrade += ' --set-file xsuaa.jsonParameters=xs-security.json'
106
127
  run(helmUpgrade)
@@ -0,0 +1,142 @@
1
+ const cds = require('../../lib/cds')
2
+ const { path } = cds.utils
3
+ const { join } = path
4
+ const { execSync } = require('node:child_process')
5
+ const { highlight } = require('../../lib/util/term')
6
+
7
+ const buildpacks = new Set([
8
+ 'java', 'nodejs', 'sap-machine', 'executable-jar', 'spring-boot', 'syft'
9
+ ])
10
+ const builders = new Set([
11
+ 'builder-jammy-base', 'builder-jammy-buildpackless-base', 'builder-jammy-full', 'builder-jammy-buildpackless-full'
12
+ ])
13
+
14
+ const keyProcessorMap = {
15
+ dockerfile: processDockerfile,
16
+ commands: processCommands,
17
+ buildpack: processBuildpack
18
+ }
19
+
20
+ const run = cmd => {
21
+ cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd
22
+ console.log(highlight(cmd))
23
+ try {
24
+ return execSync(cmd, {
25
+ stdio: 'inherit',
26
+ env: { ...process.env, FORCE_COLOR: cds.utils.colors.enabled }
27
+ })
28
+ } catch (error) {
29
+ throw error.message
30
+ }
31
+ }
32
+
33
+ function appendSubcommand(param, subcommand) {
34
+ if (!param) return []
35
+ if (subcommand === '--env') {
36
+ return Object.entries(param).flatMap(([k, v]) =>
37
+ String(v).split(',').map(env => [subcommand, `${k}=${env.trim() || '""'}`])
38
+ ).flat()
39
+ }
40
+ return param.split(',').map(e => [subcommand, e.trim()]).flat()
41
+ }
42
+
43
+ function processDockerfile(bp, name, tag) {
44
+ if (!bp.dockerfile) return []
45
+ return [['docker', 'build',
46
+ ...appendSubcommand(`${name}:${tag ?? 'latest'}`, '-t'),
47
+ ...appendSubcommand(bp.dockerfile, '-f'),
48
+ '.']]
49
+ }
50
+
51
+ function processCommands(bp) {
52
+ return bp.commands ? bp.commands.map(c => c.trim().split(' ')) : []
53
+ }
54
+ function processBuildpack(bp, name, tag, clearCache = false) {
55
+ let { type, path, env, builder } = bp.buildpack
56
+ type = type ? type.split(',').map(t => buildpacks.has(t.trim()) ? `paketo-buildpacks/${t.trim()}` : t.trim()) : []
57
+ if (builders.has(builder)) builder = `paketobuildpacks/${builder}`
58
+ const cacheArgs = clearCache ? ['--clear-cache'] : []
59
+ return [['pack',
60
+ ...appendSubcommand(`${name}:${tag}`, 'build'),
61
+ ...appendSubcommand(path, '--path'),
62
+ ...type.flatMap(t => appendSubcommand(t, '--buildpack')),
63
+ ...appendSubcommand(builder, '--builder'),
64
+ ...appendSubcommand(env, '--env'),
65
+ ...cacheArgs
66
+ ]]
67
+ }
68
+
69
+ /**
70
+ * Parses a YAML configuration file to extract Docker build and push commands for modules.
71
+ *
72
+ * @param {string} filename - The path to the YAML file to parse.
73
+ * @param {string} [repoOpt=''] - Optional repository override. If not provided, uses the repository defined in the YAML file.
74
+ * @param {boolean} [clearCache=false] - Whether to clear cache for buildpack builds.
75
+ * @returns {{
76
+ * commands: Array<{
77
+ * name: string,
78
+ * tag: string,
79
+ * buildCmd: Array<string>,
80
+ * tagCmd: Array<string>,
81
+ * pushCmd: Array<string>
82
+ * }>,
83
+ * before_all: Array<Array<string>>,
84
+ * repository: string
85
+ * }} An object containing the commands for each module, any global pre-commands, and the repository name.
86
+ * @throws {Error} If modules are not defined in the YAML file or if build-parameters are invalid.
87
+ */
88
+ function parseYAML(filename, repoOpt = '', options) {
89
+ const file = cds.load.yaml(join(cds.root, filename))
90
+ const { repository: repoFile, modules, tag: globalTag = 'latest' } = file
91
+ const repository = repoOpt || repoFile
92
+ const before_all = file['before-all'] ? file['before-all'].map(c => c.trim().split(' ')) : []
93
+ if (!modules) throw new Error('Modules not defined')
94
+ console.log(`Using repository: ${repository}`)
95
+ const commands = modules.map(m => {
96
+ const name = m.name, tag = m.tag || globalTag, bp = m['build-parameters']
97
+ const source = `${name}:${tag}`, repoImg = `${repository}/${source}`
98
+ const key = Object.keys(bp)[0]
99
+ const buildType = keyProcessorMap[key]
100
+ if (!buildType) throw new Error(`build-parameters ${Object.keys(bp)} is invalid.`)
101
+ return {
102
+ name, tag,
103
+ buildCmd: key === 'buildpack'
104
+ ? buildType(bp, name, tag, options.clearCache)
105
+ : buildType(bp, name, tag),
106
+ tagCmd: ['docker', 'tag', source, repoImg],
107
+ pushCmd: ['docker', 'push', repoImg]
108
+ }
109
+ })
110
+ return { commands, before_all, repository }
111
+ }
112
+
113
+ /**
114
+ * Executes the containerization commands for each module.
115
+ * @param {*} modules - The list of modules with their respective build, tag, and push commands.
116
+ * @param {*} options - Options object, e.g., { push: true } to push images after building.
117
+ */
118
+ async function executeModules(modules, options) {
119
+ for (const m of modules) {
120
+ console.log(`Containerising module ${m.name}`)
121
+ for (const cmd of m.buildCmd) run(cmd)
122
+ if (options.push) {
123
+ run(m.tagCmd)
124
+ run(m.pushCmd)
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Main function to containerize modules based on a YAML configuration file.
131
+ * @param {string} filename - The path to the YAML configuration file.
132
+ * @param {{ push: boolean, clearCache: boolean }} options - Options object, e.g., { push: true } to push images after building.
133
+ * @returns {Promise<void>}
134
+ */
135
+ async function containerize(filename, options = { push: false, clearCache: false }) {
136
+ const { repository, before_all, commands: modules } = parseYAML(filename, '', options)
137
+ if (!repository || !modules) return
138
+ for (const cmd of before_all) await run(cmd)
139
+ await executeModules(modules, options)
140
+ }
141
+
142
+ module.exports = { containerize }
@@ -179,7 +179,7 @@ class BuildTaskEngine {
179
179
 
180
180
  /** @private */
181
181
  _resolveHandlerResponse(results, pluginMessages = []) {
182
- /** @type {BuildError[]}*/
182
+ /** @type {(BuildError | Error)[]}*/
183
183
  const errors = []
184
184
  const resolvedResults = results.reduce((acc, r) => {
185
185
  if (r.status === 'fulfilled') {
@@ -195,7 +195,10 @@ class BuildTaskEngine {
195
195
  const error = errors.find(e => e.constructor.name !== COMPILATION_ERROR)
196
196
  if (error) {
197
197
  // throw original BuildError including existing BuildMessages
198
- error.messages = error.messages.map(m => typeof m === 'string' ? new BuildMessage(m, SEVERITY_ERROR) : m)
198
+ if (!(error instanceof BuildError)) {
199
+ throw error
200
+ }
201
+ error.messages = error.messages?.map(m => typeof m === 'string' ? new BuildMessage(m, SEVERITY_ERROR) : m)
199
202
  error.messages = this._sortMessagesUnique(pluginMessages.filter(m => m instanceof BuildMessage), error.messages)
200
203
  throw error
201
204
  }
@@ -126,7 +126,7 @@ class Plugin {
126
126
  * @abstract
127
127
  * Called by the framework to create the artifacts of this build plugin.
128
128
  */
129
- async build() { /* abstract */}
129
+ async build() { /* abstract */ }
130
130
 
131
131
  /**
132
132
  * Called by the framework immediately before 'build' to delete any output created by this build plugin.
@@ -1,6 +1,5 @@
1
1
  const cds = require('../../../cds')
2
- const yaml = require('yaml')
3
- const { exists, path, readFileSync } = cds.utils
2
+ const { exists, path, readFileSync, yaml } = cds.utils
4
3
  const { Plugin, BuildError } = require('../..')
5
4
  const { join } = path
6
5
  const { readProject } = require('../../../init/projectReader')
@@ -11,7 +10,7 @@ module.exports = class HelmBuildPlugin extends Plugin {
11
10
  static hasTask() {
12
11
  if (exists('chart/Chart.yaml')) {
13
12
  const chartFile = readFileSync(join(cds.root, 'chart', 'Chart.yaml'), 'utf8')
14
- const chartYaml = yaml.parseDocument(chartFile).toJS()
13
+ const chartYaml = yaml.parse(chartFile)
15
14
  return chartYaml.annotations?.["app.kubernetes.io/managed-by"] === 'cds-dk/helm'
16
15
  }
17
16
  return false
@@ -27,15 +26,16 @@ module.exports = class HelmBuildPlugin extends Plugin {
27
26
  }
28
27
 
29
28
  async build() {
30
- if(!HelmBuildPlugin.hasTask()) {
29
+ if (!HelmBuildPlugin.hasTask()) {
31
30
  this.pushMessage("Helm Chart not found. Execute `cds add helm` to add the helm chart.", Plugin.WARNING)
32
31
  return
33
32
  }
34
33
 
35
- const { hasApprouter, hasConnectivity, hasHtml5Repo } = readProject()
34
+ const project = readProject()
35
+ const { hasApprouter, hasConnectivity, hasHtml5Repo } = project
36
36
 
37
37
  const chartFile = readFileSync(join(cds.root, 'chart', 'Chart.yaml'), 'utf8')
38
- const chartYaml = yaml.parseDocument(chartFile).toJS()
38
+ const chartYaml = yaml.parse(chartFile)
39
39
  const isInternal = chartYaml.dependencies?.find(dep => dep.name === 'web-application')?.repository !== undefined
40
40
 
41
41
  if (!isInternal) {
@@ -66,7 +66,6 @@ module.exports = class HelmBuildPlugin extends Plugin {
66
66
  await this.copy(join(pluginResourcePath, 'connectivity/connectivity-proxy-info.yaml')).to('templates/connectivity-proxy-info.yaml')
67
67
  }
68
68
 
69
-
70
69
  if (hasHtml5Repo) {
71
70
  await this.copy(join(pluginResourcePath, 'html5-repo/html5-apps-deployer-configmap.yaml')).to('templates/html5-apps-deployer-configmap.yaml')
72
71
  }
@@ -21,6 +21,10 @@ const FILE_NAME_UNDEPLOY_JSON = "undeploy.json"
21
21
  const DEPLOY_FORMAT = "deploy-format"
22
22
  const DEBUG = cds.debug('cli|build')
23
23
 
24
+ // Some HDI artifacts do not require explicit plugin specification in _.hdiconfig_.
25
+ // This list contains exceptions which will be ignored during the consistency check.
26
+ const PLUGINLESS_HDB_FILES = new Set([".hdbgrants", ".hdbrevokes"])
27
+
24
28
  class HanaBuildPlugin extends InternalBuildPlugin {
25
29
  init() {
26
30
  if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
@@ -473,8 +477,12 @@ class HanaBuildPlugin extends InternalBuildPlugin {
473
477
  }
474
478
  }
475
479
  if (Object.keys(content['file_suffixes']).length < hdiPlugins.size) {
476
- const missingPlugins = Array.from(hdiPlugins).filter(key => !content['file_suffixes'][key.slice(1)])
477
- this.pushMessage(`'HANA database plugin not found for following file suffix [${missingPlugins.join(', ')}]`)
480
+ const missingPlugins = Array.from(hdiPlugins)
481
+ .filter(key => !content['file_suffixes'][key.slice(1)])
482
+ .filter(key => !PLUGINLESS_HDB_FILES.has(key))
483
+ if (missingPlugins.length) {
484
+ this.pushMessage(`'HANA database plugin not found for following file suffix [${missingPlugins.join(', ')}]`)
485
+ }
478
486
  }
479
487
  await this.write(content).to(hdiConfigPath)
480
488
  }
@@ -336,12 +336,12 @@ function _printUsageHint(dest, service, cwd) {
336
336
  dest = dest.replace(extension, '');
337
337
  let using
338
338
  if (service.length > 1){
339
- using = `using { ${service.join(', ')}} from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}'`;
339
+ using = `using { ${service.join(', ')}} from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}';`;
340
340
  } else {
341
- using = `using { ${service} as external } from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}'`;
341
+ using = `using { ${service} as external } from './${path.relative(cds.env.folders.srv, dest).replace(/\\/g, '/')}';`;
342
342
  }
343
343
  const message = `\n[cds] - imported API to ${path.relative(cwd, dest)}
344
- > use it in your CDS models through the like of:
344
+ > use it in your CDS models through the likes of:
345
345
 
346
346
  ${require('../util/term').info(using)}
347
347
  `;
@@ -12,7 +12,7 @@ const messages = {
12
12
  FILE_MODIFIED: 'The file with the same name exists and is been modified. Use --force flag to forcefully overwrite the file.',
13
13
  OUTPUT_FILE_MISMATCH: 'Specify the file extension that --as option supports.',
14
14
  INVALID_AS_OPTION: 'For --as option, please specify one of these output file format [cds, csn, json].',
15
- UNSUPPORTED_FROM_OPTION: 'For --from option, please specify one of these types [edmx, openapi, asyncapi, rfc]. Make sure the required importer plugin is installed.',
15
+ UNSUPPORTED_FROM_OPTION: `No importer is installed for type '{0}'. Known import kinds are: {1}. Make sure the required importer plugin is installed.`,
16
16
  INCORRECT_EXTENSION: 'If --as option is not provided, then please provide either .json or .csn output file extension.',
17
17
  INCORRECT_INPUT_NAMESPACES: 'Invalid value for --include-namespaces option. Specify namespaces as comma-separated list or "*"',
18
18
  MISSING_ASSOCIATION: 'Association is missing in the input file provided. Please check the edmx file.',
@@ -5,7 +5,7 @@ module.exports = {
5
5
  COMMAND_ADD: 'add',
6
6
 
7
7
  JAVA_LTS_VERSIONS: [17, 21],
8
- MAVEN_ARCHETYPE_VERSION: '4.4.0',
8
+ MAVEN_ARCHETYPE_VERSION: '4.4.1',
9
9
 
10
10
  OPTIONS: Object.freeze({
11
11
  COMPLETION: 'completion',
@@ -57,6 +57,8 @@ module.exports = {
57
57
  APP_FRONTEND: 'app-frontend',
58
58
  PORTAL: 'portal',
59
59
  APPLICATION_LOGGING: 'application-logging',
60
+ CLOUD_LOGGING: 'cloud-logging',
61
+ TELEMETRY: 'telemetry',
60
62
  AUDIT_LOGGING: 'audit-logging',
61
63
  NOTIFICATIONS: 'notifications',
62
64
  WORKZONE: 'workzone',
package/lib/init/index.js CHANGED
@@ -161,9 +161,7 @@ module.exports = class CDSGenerator {
161
161
  if (!cds.cli.options.dry) console.log(`adding ${term.bold(facet)}`)
162
162
  const template = this.templateList[i]
163
163
  await template.run()
164
- await template.combine()
165
164
  }
166
-
167
165
  DEBUG && console.log()
168
166
  DEBUG && console.log('plugins active for production:')
169
167
  const fromDk = readdirSync(join(__dirname, './template'))
@@ -176,7 +174,7 @@ module.exports = class CDSGenerator {
176
174
  for (const plugin of sorted) {
177
175
  const Plugin = registered.get(plugin) ?? require('./template/' + plugin)
178
176
  const template = new Plugin()
179
- const inProduction = Plugin.hasInProduction(env4('production')) || cds.cli.argv.includes(plugin) || cds.cli.options.add?.has(plugin)
177
+ const inProduction = Plugin.hasInProduction(env4('production')) || cds.cli.argv[0]?.includes(plugin) || cds.cli.options.add?.has(plugin)
180
178
  DEBUG && console.log(inProduction ? '✅' : '❌', plugin)
181
179
  if (inProduction) await template.combine()
182
180
  }
@@ -236,8 +234,8 @@ module.exports = class CDSGenerator {
236
234
  if (plugin === 'appfront') plugin = 'app-front'
237
235
  if (plugin === 'helm' && cds.cli.options['internal-unified-runtime-charts']) {
238
236
  plugin = 'helm-unified-runtime'
239
- cds.cli.options.add.delete('helm')
240
- cds.cli.options.add.add('helm-unified-runtime')
237
+ cds.cli.options.add?.delete('helm')
238
+ cds.cli.options.add?.add('helm-unified-runtime')
241
239
  }
242
240
  if (plugin === 'sample-tiny') plugin = 'tiny-sample'
243
241
  if (plugin in { 'kibana-logging': 1, 'kibana': 1 }) plugin = 'application-logging'
@@ -250,6 +248,7 @@ module.exports = class CDSGenerator {
250
248
  if (await template.canRun()) {
251
249
  templates.set(plugin, template)
252
250
  const dependencies = await template.requires()
251
+ dependencies?.forEach(d => cds.cli.options.add?.add(d))
253
252
  dependencies?.forEach(d => plugins.add(d))
254
253
  } else {
255
254
  throw 'cannot run plugin \'' + plugin + '\'' + (DEBUG ? 'Does not satisfy the canRun() function:\n ' + template.canRun : '')