@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.
- package/CHANGELOG.md +18 -2
- package/bin/add.js +3 -3
- package/bin/cds.js +16 -2
- package/bin/import.js +6 -2
- package/bin/logout.js +9 -12
- package/lib/bind/shared.js +2 -2
- package/lib/build/constants.js +0 -1
- package/lib/build/index.js +420 -463
- package/lib/build/plugins/hana/index.js +121 -2
- package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/templates/job.yaml +1 -1
- package/lib/build/plugins/helm/files/subcharts/content-deployment/values.schema.json +4 -0
- package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +5 -2
- package/lib/build/plugins/internal.js +1 -1
- package/lib/build/plugins/mtx-extension/index.js +0 -2
- package/lib/deploy/index.js +23 -3
- package/lib/deploy/to-hana/hana.js +24 -15
- package/lib/import/common.js +6 -3
- package/lib/init/constants.js +3 -1
- package/lib/init/indent.js +0 -1
- package/lib/init/merge.js +0 -1
- package/lib/init/mvn.js +4 -2
- package/lib/init/projectReader.js +0 -1
- package/lib/init/registries/mta.js +4 -2
- package/lib/init/template/app-frontend/index.js +39 -4
- package/lib/init/template/data/as-csv.js +2 -0
- package/lib/init/template/esm/index.js +5 -0
- package/lib/init/template/extensibility/index.js +1 -1
- package/lib/init/template/hana/index.js +1 -1
- package/lib/init/template/html5-repo/files/mta.yaml.hbs +9 -20
- package/lib/init/template/http/index.js +34 -6
- package/lib/init/template/kyma/index.js +1 -0
- package/lib/init/template/postgres/files/values.yaml.hbs +1 -1
- package/lib/init/template/postgres/index.js +1 -1
- package/lib/init/template/react/files/index.html +13 -0
- package/lib/init/template/react/files/package.json.hbs +22 -0
- package/lib/init/template/react/files/public/favicon.svg +1 -0
- package/lib/init/template/react/files/src/App.jsx.hbs +29 -0
- package/lib/init/template/react/files/src/index.css +28 -0
- package/lib/init/template/react/files/src/main.jsx +10 -0
- package/lib/init/template/react/files/vite.config.js +19 -0
- package/lib/init/template/react/index.js +29 -0
- package/lib/init/template/sample/index.js +1 -4
- package/lib/init/template/typer/index.js +1 -1
- package/lib/init/template/typescript/files/package.json +1 -1
- package/lib/init/template/typescript/index.js +1 -1
- package/lib/init/template/ui5/index.js +2 -1
- package/lib/init/template/vue/files/index.html +13 -0
- package/lib/init/template/vue/files/package.json.hbs +19 -0
- package/lib/init/template/vue/files/public/favicon.svg +1 -0
- package/lib/init/template/vue/files/src/App.vue.hbs +34 -0
- package/lib/init/template/vue/files/src/main.js +5 -0
- package/lib/init/template/vue/files/src/style.css +20 -0
- package/lib/init/template/vue/files/vite.config.js +19 -0
- package/lib/init/template/vue/index.js +29 -0
- package/lib/init/template/workzone-standard/files/mta.yaml.hbs +2 -13
- package/lib/init/xml.js +0 -2
- package/lib/mtx/auth_manager.js +21 -11
- package/lib/mtx/download_legacy.js +9 -8
- package/lib/mtx/pull.js +9 -8
- package/lib/mtx/push.js +42 -28
- package/lib/mtx/settings_manager.js +17 -10
- package/lib/mtx/tenant_action.js +9 -9
- package/lib/mtx/util/request.js +158 -0
- package/lib/util/cf.js +30 -29
- package/lib/watch/node-watch.js +1 -1
- package/lib/watch/vite-watch.js +76 -0
- package/lib/watch/watched.js +5 -4
- package/npm-shrinkwrap.json +14 -154
- package/package.json +1 -2
- package/lib/init/template/sample/files/db/hana/index.cds +0 -10
- package/lib/init/template/sample/files/db/sqlite/index.cds +0 -10
- package/lib/init/template/sample/files/package.json.hbs +0 -14
- 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
|
-
//
|
|
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) {
|
|
@@ -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:
|
|
@@ -14,8 +14,8 @@ global:
|
|
|
14
14
|
_internal:
|
|
15
15
|
content-deployment:
|
|
16
16
|
versions:
|
|
17
|
-
1.
|
|
18
|
-
# Will be used instead of the `.Chart.Name`, e.g. when generating the
|
|
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
|
|
@@ -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
|
}
|
package/lib/deploy/index.js
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
options: /** @type{const} */([
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
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) {
|
package/lib/import/common.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/** @type { import('@sap/cds') } */
|
|
5
5
|
const cds = require('../cds');
|
|
6
6
|
let convert = require('xml-js');
|
|
7
|
-
|
|
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
|
|
105
|
-
|
|
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 {
|
package/lib/init/constants.js
CHANGED
|
@@ -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.
|
|
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',
|
package/lib/init/indent.js
CHANGED
|
@@ -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) {
|
|
20
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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, '
|
|
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 = [{
|
|
@@ -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] : []
|
|
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 })
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
264
|
-
|
|
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
|
}
|