@sap/cds-dk 9.8.4 → 9.9.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 (74) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/bin/add.js +3 -3
  3. package/bin/cds.js +16 -2
  4. package/bin/import.js +6 -2
  5. package/bin/logout.js +9 -12
  6. package/lib/bind/shared.js +2 -2
  7. package/lib/build/constants.js +0 -1
  8. package/lib/build/index.js +420 -463
  9. package/lib/build/plugins/hana/index.js +121 -2
  10. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +1 -1
  11. package/lib/build/plugins/helm/files/subcharts/content-deployment/templates/job.yaml +1 -1
  12. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.schema.json +4 -0
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +5 -2
  14. package/lib/build/plugins/internal.js +1 -1
  15. package/lib/build/plugins/mtx-extension/index.js +0 -2
  16. package/lib/deploy/index.js +23 -3
  17. package/lib/deploy/to-hana/hana.js +24 -15
  18. package/lib/import/common.js +6 -3
  19. package/lib/init/constants.js +3 -1
  20. package/lib/init/indent.js +0 -1
  21. package/lib/init/merge.js +0 -1
  22. package/lib/init/mvn.js +4 -2
  23. package/lib/init/projectReader.js +0 -1
  24. package/lib/init/registries/mta.js +4 -2
  25. package/lib/init/template/app-frontend/index.js +39 -4
  26. package/lib/init/template/data/as-csv.js +2 -0
  27. package/lib/init/template/esm/index.js +5 -0
  28. package/lib/init/template/extensibility/index.js +1 -1
  29. package/lib/init/template/hana/index.js +1 -1
  30. package/lib/init/template/html5-repo/files/mta.yaml.hbs +9 -20
  31. package/lib/init/template/http/index.js +34 -6
  32. package/lib/init/template/kyma/index.js +1 -0
  33. package/lib/init/template/postgres/files/values.yaml.hbs +1 -1
  34. package/lib/init/template/postgres/index.js +1 -1
  35. package/lib/init/template/react/files/index.html +13 -0
  36. package/lib/init/template/react/files/package.json.hbs +22 -0
  37. package/lib/init/template/react/files/public/favicon.svg +1 -0
  38. package/lib/init/template/react/files/src/App.jsx.hbs +29 -0
  39. package/lib/init/template/react/files/src/index.css +28 -0
  40. package/lib/init/template/react/files/src/main.jsx +10 -0
  41. package/lib/init/template/react/files/vite.config.js +19 -0
  42. package/lib/init/template/react/index.js +29 -0
  43. package/lib/init/template/sample/index.js +1 -4
  44. package/lib/init/template/typer/index.js +1 -1
  45. package/lib/init/template/typescript/files/package.json +1 -1
  46. package/lib/init/template/typescript/index.js +1 -1
  47. package/lib/init/template/ui5/index.js +2 -1
  48. package/lib/init/template/vue/files/index.html +13 -0
  49. package/lib/init/template/vue/files/package.json.hbs +19 -0
  50. package/lib/init/template/vue/files/public/favicon.svg +1 -0
  51. package/lib/init/template/vue/files/src/App.vue.hbs +34 -0
  52. package/lib/init/template/vue/files/src/main.js +5 -0
  53. package/lib/init/template/vue/files/src/style.css +20 -0
  54. package/lib/init/template/vue/files/vite.config.js +19 -0
  55. package/lib/init/template/vue/index.js +29 -0
  56. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +2 -13
  57. package/lib/init/xml.js +0 -2
  58. package/lib/mtx/auth_manager.js +21 -11
  59. package/lib/mtx/download_legacy.js +9 -8
  60. package/lib/mtx/pull.js +9 -8
  61. package/lib/mtx/push.js +42 -28
  62. package/lib/mtx/settings_manager.js +17 -10
  63. package/lib/mtx/tenant_action.js +9 -9
  64. package/lib/mtx/util/request.js +158 -0
  65. package/lib/util/cf.js +30 -29
  66. package/lib/watch/node-watch.js +1 -1
  67. package/lib/watch/vite-watch.js +76 -0
  68. package/lib/watch/watched.js +5 -4
  69. package/npm-shrinkwrap.json +14 -154
  70. package/package.json +1 -2
  71. package/lib/init/template/sample/files/db/hana/index.cds +0 -10
  72. package/lib/init/template/sample/files/db/sqlite/index.cds +0 -10
  73. package/lib/init/template/sample/files/package.json.hbs +0 -14
  74. package/lib/util/axios.js +0 -118
@@ -9,6 +9,7 @@ const DEFAULT_COMPILE_DEST_FOLDER = path.normalize("src/gen")
9
9
  const FILE_EXT_CSV = ".csv"
10
10
  const FILE_EXT_HDBTABLEDATA = ".hdbtabledata"
11
11
  const FILE_EXT_HDBTABLE = ".hdbtable"
12
+ const FILE_EXT_HDBSYNONYMCONFIG = ".hdbsynonymconfig"
12
13
  const FILE_EXT_HDBMIGRATIONTABLE = ".hdbmigrationtable"
13
14
  const FILE_NAME_HDICONFIG = ".hdiconfig"
14
15
  const FILE_NAME_HDINAMESPACE = ".hdinamespace"
@@ -70,7 +71,8 @@ module.exports = class HanaBuildPlugin extends require('../internal') {
70
71
  await this._writeHdiConfig(hdiPlugins)
71
72
  await this._writeHdiNamespace()
72
73
 
73
- // TODO disabled as this contradicts the MTX domain concept which allows partial app deployments
74
+ // cds10: disabled as it did not work with MTX domain concept which allows partial app deployments
75
+ // consider enabling this in cds10
74
76
  //await this._writeUndeployJson()
75
77
 
76
78
  if (!this.hasBuildOption(CONTENT_HDBTABLEDATA, false)) {
@@ -100,6 +102,13 @@ module.exports = class HanaBuildPlugin extends require('../internal') {
100
102
  })
101
103
  )
102
104
  }
105
+
106
+ // relocate .hdbsynonymconfig files containing *.configure properties to cfg/ folder
107
+ // HDI's template mechanism for configuration files only works from the cfg/ folder
108
+ if (this.isStagingBuild()) {
109
+ const relocatedDirs = await this._relocateSynonymConfigsToCfg()
110
+ await this._writeCfgHdiConfig(relocatedDirs)
111
+ }
103
112
  }
104
113
  }
105
114
 
@@ -561,7 +570,6 @@ module.exports = class HanaBuildPlugin extends require('../internal') {
561
570
  if (!this.hasBuildOption(CONTINUE_UNRESOLVED_SCHEMA_CHANGES, true)) {
562
571
  throw new BuildError('Current model changes require manual resolution', resolutionMessages)
563
572
  }
564
- // adding to the task's message list
565
573
  resolutionMessages.forEach(m => this._messages.push(m))
566
574
  }
567
575
  }
@@ -605,6 +613,117 @@ module.exports = class HanaBuildPlugin extends require('../internal') {
605
613
 
606
614
  return workspaceFiles
607
615
  }
616
+
617
+ /**
618
+ * Relocates .hdbsynonymconfig files containing *.configure properties (e.g. schema.configure,
619
+ * database.configure) from the dest src/ folder to the dest cfg/ folder.
620
+ * HDI's template mechanism for configuration files only works when the config files reside
621
+ * in the cfg/ folder. The HDI deployer scans cfg/ at startup, detects *.configure markers,
622
+ * and applies runtime substitution from bound service credentials.
623
+ */
624
+ async _relocateSynonymConfigsToCfg() {
625
+ const destSrcDir = path.join(this.task.dest, 'src')
626
+ const destCfgDir = path.join(this.task.dest, 'cfg')
627
+
628
+ const synonymConfigFiles = await find(destSrcDir, { filter: (entry) => {
629
+ if (isdir(entry)) {
630
+ return true
631
+ }
632
+ return path.extname(entry) === FILE_EXT_HDBSYNONYMCONFIG
633
+ }})
634
+
635
+ // Track which directories under cfg/ received relocated files and their extensions
636
+ const relocatedDirs = new Map() // dir -> Set of file extensions
637
+ await Promise.all(synonymConfigFiles.map(async (file) => {
638
+ if (await _hasConfigureProperties(file)) {
639
+ const relPath = path.relative(destSrcDir, file)
640
+ const cfgDest = path.join(destCfgDir, relPath)
641
+ DEBUG?.(`relocating ${path.relative(this.task.dest, file)} to cfg/ folder (contains .configure properties)`)
642
+ await fs.promises.mkdir(path.dirname(cfgDest), { recursive: true })
643
+ await fs.promises.rename(file, cfgDest)
644
+ this.pushFile(cfgDest)
645
+
646
+ const cfgDestDir = path.dirname(cfgDest)
647
+ if (!relocatedDirs.has(cfgDestDir)) {
648
+ relocatedDirs.set(cfgDestDir, new Set())
649
+ }
650
+ relocatedDirs.get(cfgDestDir).add(path.extname(file))
651
+ }
652
+ }))
653
+ return relocatedDirs
654
+ }
655
+
656
+ /**
657
+ * Create .hdiconfig files in cfg/ subdirectories that received relocated config files.
658
+ * This ensures that the HDI deployer can recognize the file types in those directories.
659
+ * Only creates .hdiconfig in directories where one does not already exist.
660
+ * @param {Map<string, Set<string>>} relocatedDirs Map of directory paths to Sets of file extensions
661
+ */
662
+ async _writeCfgHdiConfig(relocatedDirs) {
663
+ if (!relocatedDirs || relocatedDirs.size === 0) {
664
+ return
665
+ }
666
+ // Collect all relocated file extensions across all directories
667
+ const allExtensions = new Set()
668
+ for (const extensions of relocatedDirs.values()) {
669
+ for (const ext of extensions) {
670
+ allExtensions.add(ext)
671
+ }
672
+ }
673
+
674
+ const template = await _readTemplateAsJson('.hdiconfig-all')
675
+ const content = { 'file_suffixes': {} }
676
+ for (const key in template['file_suffixes']) {
677
+ if (allExtensions.has('.' + key)) {
678
+ content['file_suffixes'][key] = template['file_suffixes'][key]
679
+ }
680
+ }
681
+ if (Object.keys(content['file_suffixes']).length === 0) {
682
+ return
683
+ }
684
+
685
+ // Write .hdiconfig to each distinct directory under cfg/ that received relocated files,
686
+ // but only if one does not already exist
687
+ for (const dir of relocatedDirs.keys()) {
688
+ const hdiConfigPath = path.join(dir, FILE_NAME_HDICONFIG)
689
+ if (!fs.existsSync(hdiConfigPath)) {
690
+ await this.write(content).to(hdiConfigPath)
691
+ }
692
+ }
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Checks whether an .hdbsynonymconfig file contains any *.configure properties.
698
+ * These are HDI template markers (e.g. schema.configure, database.configure) that
699
+ * require the file to be placed in the cfg/ folder for HDI's runtime substitution.
700
+ * @param {string} file - absolute path to the .hdbsynonymconfig file
701
+ * @returns {Promise<boolean>} true if the file contains .configure properties
702
+ */
703
+ async function _hasConfigureProperties(file) {
704
+ try {
705
+ const content = JSON.parse((await fs.promises.readFile(file, 'utf-8')).toString())
706
+ return _objectHasConfigureProperty(content)
707
+ // eslint-disable-next-line no-unused-vars
708
+ } catch (e) {
709
+ // if the file cannot be parsed, leave it in place
710
+ return false
711
+ }
712
+ }
713
+
714
+ function _objectHasConfigureProperty(obj) {
715
+ if (obj === null || typeof obj !== 'object') {
716
+ return false
717
+ }
718
+ for (const key of Object.keys(obj)) {
719
+ if (key.endsWith('.configure')) {
720
+ return true
721
+ }
722
+ if (typeof obj[key] === 'object' && _objectHasConfigureProperty(obj[key])) {
723
+ return true
724
+ }
725
+ }
726
+ return false
608
727
  }
609
728
 
610
729
  async function _readTemplateAsJson(template) {
@@ -4,4 +4,4 @@ name: content-deployment
4
4
  sources:
5
5
  - https://github.tools.sap/unified-runtime/content-deployment
6
6
  type: application
7
- version: 1.9.0
7
+ version: 1.10.0
@@ -71,7 +71,7 @@ spec:
71
71
  {{- if .Values.runAsUser }}
72
72
  runAsUser: {{ .Values.runAsUser }}
73
73
  {{- end }}
74
- readOnlyRootFilesystem: false
74
+ readOnlyRootFilesystem: {{ $.Values.readOnlyRootFilesystem | default false }}
75
75
  appArmorProfile:
76
76
  type: RuntimeDefault
77
77
  volumeMounts:
@@ -67,6 +67,10 @@
67
67
  "type": "integer",
68
68
  "minimum": 1
69
69
  },
70
+ "readOnlyRootFilesystem": {
71
+ "$id": "#/properties/readOnlyRootFilesystem",
72
+ "type": "boolean"
73
+ },
70
74
  "image": {
71
75
  "$id": "#/properties/image",
72
76
  "title": "Image configuration",
@@ -14,8 +14,8 @@ global:
14
14
  _internal:
15
15
  content-deployment:
16
16
  versions:
17
- 1.9.0: true
18
- # Will be used instead of the `.Chart.Name`, e.g. when generating the Deployment name.
17
+ 1.10.0: true
18
+ # Will be used instead of the `.Chart.Name`, e.g. when generating the Job name.
19
19
  # nameOverride: name
20
20
 
21
21
  # Will be used as an override for the `.fullname` helper function (i.e. `.Release.Name-.Chart.Name`).
@@ -26,6 +26,9 @@ global:
26
26
 
27
27
  # The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified.
28
28
  # runAsUser: 1000
29
+
30
+ # Use read only filesystem for the Job.
31
+ # readOnlyRootFilesystem: false
29
32
  image: {}
30
33
  # Should also include the image name (i.e. everything before the `:` sign).
31
34
  # repository: repo/image
@@ -9,7 +9,7 @@ const TRACE = cds.debug('trace')
9
9
  module.exports = class InternalBuildPlugin extends require('./plugin') {
10
10
 
11
11
  init() {
12
- // REVISIT: no default gen folder for now
12
+ // no default gen folder
13
13
  }
14
14
 
15
15
  async clean() {
@@ -26,7 +26,6 @@ module.exports = class MtxExtensionBuildPlugin extends require('../internal') {
26
26
  let model
27
27
  let extensionModel
28
28
 
29
- // REVISIT: check existence of SaaS app package folder only
30
29
  // cds.resolve will fail as no index.csn file exists in this folder for the extension migration use case
31
30
  // a compilation error is thrown anyhow if any base model refs cannot be resolved
32
31
 
@@ -134,7 +133,6 @@ module.exports = class MtxExtensionBuildPlugin extends require('../internal') {
134
133
 
135
134
  const messages = linter.lint(extModel, model, env)
136
135
  if (messages.length) {
137
- // REVISIT: lint messages can be passed as is with cds-mtxs version >= 1.7
138
136
  throw new BuildError('SaaS extension point restrictions violated. Check the concrete restrictions defined by the SaaS app provider.',
139
137
  messages.map(f => new BuildMessage(f.message, f.severity, f.location || f.element?.$location)))
140
138
  }
@@ -1,5 +1,25 @@
1
1
  module.exports = {
2
- options: /** @type{const} */(['--to', '--for', '--out', '--on', '--tunnel-address', '--vcap-file', '--delta-from']),
3
- flags: /** @type{const} */(['--no-save', '--auto-undeploy', '--dry', '--xdry', '--with-mocks', '--store-credentials', '--no-build', '--model-only', '--script', '--with-auto-schema-evolution', '--resolve-bindings']),
4
- shortcuts: ['-2', '-4', '-o', '-n']
2
+ options: /** @type{const} */([
3
+ '--to',
4
+ '--for',
5
+ '--out',
6
+ '--on',
7
+ '--tunnel-address',
8
+ '--vcap-file',
9
+ '--delta-from'
10
+ ]),
11
+ flags: /** @type{const} */([
12
+ '--resolve-bindings',
13
+ '--no-save',
14
+ '--auto-undeploy',
15
+ '--dry',
16
+ '--xdry',
17
+ '--with-mocks',
18
+ '--store-credentials',
19
+ '--no-build',
20
+ '--model-only',
21
+ '--script',
22
+ '--with-auto-schema-evolution'
23
+ ]),
24
+ shortcuts: ['-2', '-4', '-o', '-n', '', '', '', '-b']
5
25
  }
@@ -51,6 +51,27 @@ module.exports = new class HanaDeployer {
51
51
  return result;
52
52
  }
53
53
 
54
+ _applyServiceKeyToVcap(base, cfServiceInstanceName, serviceKey) {
55
+ const hanas = Array.isArray(base.hana) ? base.hana : [];
56
+ const hanaTagged = hanas.filter(s => s?.name === cfServiceInstanceName);
57
+ if (hanaTagged.length === 0) {
58
+ const allHanaTagged = hanas.filter(s => Array.isArray(s?.tags) && s.tags.includes('hana'));
59
+ const entry = allHanaTagged.length === 1
60
+ ? allHanaTagged[0]
61
+ : { name: cfServiceInstanceName, label: 'hana', tags: ['hana'], credentials: {} };
62
+ if (!hanas.includes(entry)) {
63
+ hanas.push(entry);
64
+ }
65
+ hanaTagged.push(entry);
66
+ }
67
+ for (const entry of hanaTagged) {
68
+ entry.name ??= cfServiceInstanceName;
69
+ entry.tags = Array.from(new Set([...entry.tags || [], 'hana']));
70
+ entry.credentials = { ...(entry.credentials || {}), ...serviceKey };
71
+ }
72
+ return { ...base, hana: hanas };
73
+ }
74
+
54
75
  async deploy(options = {}) {
55
76
  if (options['store-credentials']) {
56
77
  console.warn(warn('The option \'--store-credentials\' has been deprecated and will be removed with the next major release. Use command \'cds bind\' instead.'));
@@ -214,21 +235,9 @@ spec:
214
235
  serviceKeyName ??= cfServiceInstanceKeyName
215
236
  if (!vcapEnv.VCAP_SERVICES || serviceName) {
216
237
  const base = process.env.VCAP_SERVICES ? JSON.parse(process.env.VCAP_SERVICES) : (vcapEnv.VCAP_SERVICES ?? {});
217
- const hanas = Array.isArray(base.hana) ? base.hana : [];
218
- let hanaTagged = hanas.filter(s => s?.name === cfServiceInstanceName || (Array.isArray(s?.tags) && s.tags.includes('hana')));
219
- if (hanaTagged.length === 0) {
220
- const entry = { name: cfServiceInstanceName, label: 'hana', tags: ['hana'], credentials: {} };
221
- hanas.push(entry);
222
- hanaTagged = [entry];
223
- }
224
- for (const entry of hanaTagged) {
225
- entry.name ??= cfServiceInstanceName;
226
- entry.tags = Array.from(new Set([...entry.tags || [], 'hana']));
227
- entry.credentials = { ...(entry.credentials || {}), ...serviceKey };
228
- }
229
- base.hana = hanas;
230
- vcapEnv.VCAP_SERVICES = base;
231
- process.env.VCAP_SERVICES = JSON.stringify(base);
238
+ const result = this._applyServiceKeyToVcap(base, cfServiceInstanceName, serviceKey);
239
+ vcapEnv.VCAP_SERVICES = result;
240
+ process.env.VCAP_SERVICES = JSON.stringify(result);
232
241
  }
233
242
 
234
243
  if (!hasVCAPEnv) {
@@ -4,7 +4,7 @@
4
4
  /** @type { import('@sap/cds') } */
5
5
  const cds = require('../cds');
6
6
  let convert = require('xml-js');
7
- let axios = require('axios');
7
+
8
8
  const messages = require("./message").getMessages();
9
9
  const { fs, path, write, mkdirp, exists, read } = cds.utils;
10
10
  const cdsCompiler = require('..').compile.to;
@@ -101,8 +101,11 @@ async function getContentFromFile(path) {
101
101
 
102
102
  async function getContentFromURL(url) {
103
103
  let edmx;
104
- const response = await axios.get(url);
105
- edmx = response["data"];
104
+ const response = await fetch(url);
105
+ if (!response.ok) {
106
+ throw new Error(`${response.status} ${response.statusText}: while fetching ${url}`);
107
+ }
108
+ edmx = await response.text();
106
109
  if (edmx && (await _isValidXML(edmx))) {
107
110
  return edmx;
108
111
  } else {
@@ -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.8.0',
8
+ MAVEN_ARCHETYPE_VERSION: '4.8.1',
9
9
 
10
10
  OPTIONS: Object.freeze({
11
11
  COMPLETION: 'completion',
@@ -64,6 +64,8 @@ module.exports = {
64
64
  WORKZONE: 'workzone',
65
65
  WORKZONE_STANDARD: 'workzone-standard',
66
66
  UI5: 'ui5',
67
+ VUE: 'vue',
68
+ REACT: 'react',
67
69
  DATA: 'data',
68
70
  HTTP: 'http',
69
71
  LINT: 'lint',
@@ -21,7 +21,6 @@ function gcd (xs) {
21
21
  * Once that has been determined, it derives the spacing by calculating the gcd of all relevant lines.
22
22
  * I.e. if there are lines indented with 2, 4, or 8 spaces, it will find ' ' as indent.
23
23
  * If lines are indented using 1 or 2 tabs, '\t' will be returned.
24
- * TODO: filter out outliers, i.e. if the user used three spaces in one line, should ' ' space be used for spacing? Or should that be ignored?
25
24
  * @param {string[]} file - lines of a file.
26
25
  * @returns {string | null} - the string used to space (e.g. 2 spaces, 4 spaces, 1 tab, etc.) or null if the file was empty.
27
26
  */
package/lib/init/merge.js CHANGED
@@ -155,7 +155,6 @@ async function _mergeYAML(into, from, project, semantics = {}) {
155
155
  if (Array.isArray(entry)) entry = entry[0]
156
156
  const neededParent = dict.get(entry.in[0])?.node
157
157
  return collectionStack.includes(neededParent) // lookbehind in parent collection stack
158
- // REVISIT: Only look behind until sequence is reached?
159
158
  }).forEach(item => {
160
159
  if (Array.isArray(item)) item = item[0]
161
160
  const json = JSON.parse(String(node))
package/lib/init/mvn.js CHANGED
@@ -16,8 +16,10 @@ module.exports.add = async function (feature, featureOptions={}) {
16
16
 
17
17
  params.set(`com.sap.cds:cds-maven-plugin:${archetypeVersion}:add`)
18
18
 
19
- if (feature === HANA) { // REVISIT: compat for HANA until -Dfeature HANA is provided by Java
20
- feature = `CF` // REVISIT: Should get rid of CF dependency for Kyma scenarios
19
+ if (feature === HANA) {
20
+ // REVISIT: compat for HANA until -Dfeature HANA is provided by Java
21
+ // Should get rid of CF dependency for Kyma scenarios
22
+ feature = `CF`
21
23
  }
22
24
  params.set(`-Dfeature`, feature)
23
25
 
@@ -163,7 +163,6 @@ module.exports = new class ProjectReader {
163
163
  hasUIEmbedded: _uiEmbedded,
164
164
  hasUIModule: _uiModule,
165
165
  hasUI: () => _uiEmbedded() || _uiModule(),
166
- hasUI5: () => _uiEmbedded() || _uiModule(),
167
166
  literal: () => () => text => text,
168
167
  hasMTXRoute: () => _inProd('extensibility') || (_inProd('multitenancy') && !_isJava()),
169
168
  hasMTXRouteJava: () => (_inProd('helm-unified-runtime') || _inProd('helm')) && _isJava() && _inProd('multitenancy'),
@@ -3,8 +3,10 @@ const cds = require('../../cds')
3
3
  const srvNode4 = path => ({ in: 'modules', where: { type: 'nodejs', path } })
4
4
  const srvJava4 = path => ({ in: 'modules', where: { type: 'java', path } })
5
5
 
6
- // REVISIT: Clean up
7
- const isJava = process.argv[process.argv.indexOf('--add') + 1].split(/[,\s+]/).includes('java') || cds.env['project-nature'] === 'java' || cds.cli.options?.add?.has('java') || process.argv.includes('--java')
6
+ const isJava = process.argv[process.argv.indexOf('--add') + 1].split(/[,\s+]/).includes('java') ||
7
+ cds.env['project-nature'] === 'java' ||
8
+ cds.cli.options?.add?.has('java') ||
9
+ process.argv.includes('--java')
8
10
 
9
11
  const srv4 = isJava ? srvJava4 : srvNode4
10
12
 
@@ -1,10 +1,9 @@
1
- const { join } = require('node:path')
2
1
  const { readProject } = require('../../projectReader')
3
2
  const { merge } = require('../../merge')
4
3
  const { srv4, appDeployer, appFront, xsuaa } = require('../../registries/mta')
5
4
  const { odataApi } = require('../../registries/xs-app')
6
5
  const { renderAndCopy } = require('../../template')
7
- const { exists } = require('../../../cds').utils
6
+ const { sh:$, path:{join}, fs:{promises:{rename}}, write, read, exists } = require('../../../cds').utils
8
7
 
9
8
  module.exports = class extends require('../../plugin') {
10
9
 
@@ -37,17 +36,26 @@ module.exports = class extends require('../../plugin') {
37
36
  }
38
37
 
39
38
  // app deployer requires a manifest.json
39
+ const manifestRoot = hasUI5 ? 'webapp' : 'public'
40
40
  await Promise.all(appUIPaths.map(p =>
41
41
  merge(__dirname, 'files/manifest.json')
42
- .into(join(appPath, p, 'webapp/manifest.json'), { project })
42
+ .into(join(appPath, p, manifestRoot, 'manifest.json'), { project })
43
43
  ))
44
+ // manifest.json IDs must be unique -> otherwise deployment failures on conflicts
45
+ await Promise.all(project.appUIPaths.map(async p => {
46
+ const manifest = await read(join(appPath, p, manifestRoot, 'manifest.json'))
47
+ manifest['sap.app'].id = project.appName + '.' + p
48
+ await write(join(appPath, p, manifestRoot, 'manifest.json'), manifest, { spaces: 2 })
49
+ }))
50
+
44
51
  }
45
52
 
46
53
  async combine() {
47
54
  const project = readProject()
48
55
  const {
49
56
  srvPath, appPath, apps,
50
- hasMta, addMta, hasXsuaa, addSample, addApprouter, addWorkzoneStandard, addXsuaa, addIas
57
+ hasMta, addMta, hasXsuaa, addSample, addApprouter, addWorkzoneStandard, addXsuaa, addIas,
58
+ addVue, addReact, addAppFrontend
51
59
  } = project
52
60
 
53
61
  await Promise.all(apps.map(async ({ app }) => {
@@ -91,6 +99,33 @@ module.exports = class extends require('../../plugin') {
91
99
  })
92
100
  }
93
101
 
102
+ // Vue + React
103
+ const promises = []
104
+ const cds = require('../../../cds')
105
+ if (addAppFrontend || addVue || addReact) {
106
+ for (const { app } of apps) {
107
+ const appPath = join(cds.root, cds.env.folders.app, app)
108
+ const pJson = await read(join(appPath, 'package.json'))
109
+ if (!pJson.devDependencies?.vite) continue
110
+ const addVite = cds.cli.argv[0] === 'react' || cds.cli.argv[0] === 'vue'
111
+ if (addVite && cds.cli.options.into !== app) continue
112
+
113
+ // Move xs-app.json and manifest.json to /public to be included in /dist
114
+ const fromXsApp = join(appPath, 'xs-app.json')
115
+ const toXsApp = join(appPath, 'public/xs-app.json')
116
+ if (exists(fromXsApp) && !exists(toXsApp)) {
117
+ promises.push(rename(fromXsApp, toXsApp))
118
+ }
119
+ const fromManifest = join(appPath, 'webapp/manifest.json')
120
+ const toManifest = join(appPath, 'public/manifest.json')
121
+ if (exists(fromManifest) && !exists(toManifest)) {
122
+ promises.push(rename(fromManifest, toManifest))
123
+ }
124
+ $`npm install --prefix ${appPath} adm-zip --save-dev` // required for zipping /dist in the Vite build step
125
+ }
126
+ }
127
+ await Promise.all(promises)
128
+
94
129
  // When Helm support is added
95
130
  // if (hasContainerize || hasKyma) {
96
131
  // const additions = [{
@@ -60,6 +60,8 @@ module.exports = async (data={}, csn, { headerOnly=false, separator=',' }={}) =>
60
60
  if (!headerOnly)
61
61
  res += '\n' + records.join('\n')
62
62
 
63
+ if (!res.endsWith('\n')) res += '\n'
64
+
63
65
  data[entity.name] = res
64
66
 
65
67
  })
@@ -1,3 +1,4 @@
1
+ const { NODEJS } = require('../../constants').OPTIONS
1
2
  const { readProject } = require('../../projectReader')
2
3
  const { merge } = require('../../merge')
3
4
 
@@ -7,6 +8,10 @@ module.exports = class EsmTemplate extends require('../../plugin') {
7
8
  return 'ESM-compatible Node.js project'
8
9
  }
9
10
 
11
+ requires() {
12
+ return [ NODEJS ]
13
+ }
14
+
10
15
  async run() {
11
16
  const project = readProject()
12
17
  const { configFile } = project
@@ -11,7 +11,7 @@ module.exports = class ExtensibilityTemplate extends require('../../plugin') {
11
11
  }
12
12
 
13
13
  requires() {
14
- return exists('pom.xml') ? [MULTITENANCY] : [] // REVISIT: Remove this dependency
14
+ return exists('pom.xml') ? [MULTITENANCY] : []
15
15
  }
16
16
 
17
17
  static hasInProduction(env) {
@@ -31,7 +31,7 @@ module.exports = class HanaTemplate extends require('../../plugin') {
31
31
  await mvn.add('hana')
32
32
  }
33
33
 
34
- await merge(__dirname, 'files/package.json.hbs').into(configFile, { with: project, forceOverwrite: true }) // REVISIT: forceOverwrite: true as default?
34
+ await merge(__dirname, 'files/package.json.hbs').into(configFile, { with: project, forceOverwrite: true })
35
35
 
36
36
  const packageJson = exists('package.json') ? await read('package.json') : null
37
37
  if (packageJson?.dependencies) {
@@ -45,28 +45,17 @@ modules:
45
45
  HTML5Runtime_enabled: true{{#hasIas}}
46
46
  IASDependencyName: {{appName}}-ias-api{{/hasIas}}
47
47
  {{#hasWorkzoneStandard}}
48
- {{#hasMultitenancy}}
49
48
  parameters:
50
49
  config:
51
- HTML5Runtime_enabled: true
52
- init_data:
53
- instance:
54
- existing_destinations_policy: update
55
- destinations:
56
- - Name: srv-api
57
- URL: ~{srv-api/srv-url}
58
- Authentication: NoAuthentication
59
- Type: HTTP
60
- ProxyType: Internet
61
- HTML5.ForwardAuthToken: true
62
- HTML5.DynamicDestination: true
63
- {{#hasIas}}HTML5.IASDependencyName: {{appName}}-ias-api{{/hasIas}}
64
- - Name: ui5
65
- URL: https://ui5.sap.com
66
- Authentication: NoAuthentication
67
- Type: HTTP
68
- ProxyType: Internet
69
- {{/hasMultitenancy}}
50
+ destinations:
51
+ - Name: srv-api
52
+ URL: ~{srv-api/srv-url}
53
+ Authentication: NoAuthentication
54
+ Type: HTTP
55
+ ProxyType: Internet
56
+ HTML5.ForwardAuthToken: true
57
+ HTML5.DynamicDestination: true
58
+ {{#hasIas}}HTML5.IASDependencyName: {{appName}}-ias-api{{/hasIas}}
70
59
  {{/hasWorkzoneStandard}}
71
60
 
72
61
  {{#apps}}
@@ -1,5 +1,4 @@
1
1
  const https = require('node:https')
2
- const axios = require('axios')
3
2
  const asJson = require("../data/as-json")
4
3
  const cds = require('../../../cds')
5
4
  const cf = require('../../../util/cf')
@@ -255,13 +254,42 @@ module.exports = class HttpTemplate extends require('../../plugin') {
255
254
  const { url, clientid, clientsecret, certurl, certificate, key } = credentials
256
255
  if (!url) throw new Error('No authentication service found in the environment of ' + app)
257
256
 
258
- const auth = certificate ? { maxRedirects: 0, httpsAgent: new https.Agent({ cert: certificate, key }) }
259
- : { auth: { username: clientid, password: clientsecret } }
260
257
  const suffix = xsuaa ? 'oauth/token' : 'oauth2/token'
261
- const authUrl = `${certurl ?? url}/${suffix}`
258
+ const authUrl = new URL(`${certurl ?? url}/${suffix}`)
262
259
  const data = `grant_type=client_credentials&client_id=${encodeURI(clientid)}`
263
- const config = { method: 'POST', timeout: 5000, data, ...auth }
264
- const { access_token } = (await axios(authUrl, config)).data
260
+ const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
261
+ let access_token
262
+ if (certificate) {
263
+ const raw = await new Promise((resolve, reject) => {
264
+ const req = https.request({
265
+ hostname: authUrl.hostname,
266
+ port: authUrl.port || 443,
267
+ path: authUrl.pathname + authUrl.search,
268
+ cert: certificate, key,
269
+ method: 'POST',
270
+ headers
271
+ }, res => {
272
+ const chunks = []
273
+ res.on('data', x => chunks.push(x))
274
+ res.on('end', () => {
275
+ if (res.statusCode >= 200 && res.statusCode < 300) resolve(Buffer.concat(chunks).toString())
276
+ else reject(new Error(`Failed to fetch access token: ${res.statusCode}`))
277
+ })
278
+ })
279
+ req.on('error', reject)
280
+ req.end(data)
281
+ });
282
+ ({ access_token } = JSON.parse(raw))
283
+ } else {
284
+ headers['Authorization'] = 'Basic ' + Buffer.from(`${clientid}:${clientsecret}`).toString('base64')
285
+ const response = await fetch(authUrl.toString(), {
286
+ method: 'POST',
287
+ headers,
288
+ body: data
289
+ })
290
+ if (!response.ok) throw new Error(`Failed to fetch access token: ${response.status} ${response.statusText}`)
291
+ ;({ access_token } = await response.json())
292
+ }
265
293
  const approuterAuth = `x-approuter-authorization: bearer ${access_token}`
266
294
  return [hostname, approuterAuth]
267
295
  }
@@ -16,6 +16,7 @@ module.exports = class KymaTemplate extends require('../../plugin') {
16
16
  },
17
17
  'y': {
18
18
  type: 'boolean',
19
+ help: 'Skip interactive prompts.'
19
20
  }
20
21
  }
21
22
  }
@@ -3,7 +3,7 @@ srv:
3
3
  bindings:
4
4
  postgres:
5
5
  serviceInstanceName: postgres
6
- postgres-db-deployer:
6
+ postgres-deployer:
7
7
  image:
8
8
  repository: {{appName}}-postgres-deployer
9
9
  bindings: