@sap/cds-dk 9.7.2 → 9.8.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 (105) hide show
  1. package/CHANGELOG.md +35 -2
  2. package/bin/bind.js +7 -2
  3. package/bin/build.js +14 -4
  4. package/bin/cds.js +2 -2
  5. package/bin/completion/completionFs.js +1 -1
  6. package/bin/completion/index.js +2 -2
  7. package/bin/data-products/get-csn-via-dp-cli.js +1 -1
  8. package/bin/debug.js +1 -1
  9. package/bin/deploy/index.js +3 -2
  10. package/bin/gen.js +1 -1
  11. package/bin/help.js +1 -1
  12. package/bin/import.js +3 -0
  13. package/bin/minify.js +1 -1
  14. package/bin/mock.js +1 -1
  15. package/bin/repl.js +212 -39
  16. package/bin/test.js +0 -2
  17. package/bin/up.js +27 -3
  18. package/bin/util/containerize.js +1 -3
  19. package/bin/version.js +27 -7
  20. package/bin/watch.js +20 -4
  21. package/lib/bind/cf.js +2 -2
  22. package/lib/bind/k8s.js +0 -1
  23. package/lib/bind/plugin.js +2 -0
  24. package/lib/bind/shared.js +6 -1
  25. package/lib/build/constants.js +0 -8
  26. package/lib/build/index.js +569 -25
  27. package/lib/build/{provider/edmxBuildPlugin.js → plugins/edmx.js} +6 -15
  28. package/lib/build/{provider/featureToggleBuildPlugin.js → plugins/featureToggles.js} +3 -23
  29. package/lib/build/{provider → plugins}/fiori/index.js +27 -38
  30. package/lib/build/{provider → plugins}/hana/index.js +61 -83
  31. package/lib/build/{provider → plugins}/hana/migrationtable.js +4 -3
  32. package/lib/build/plugins/helm/index.js +6 -6
  33. package/lib/build/{provider/internalBuildPlugin.js → plugins/internal.js} +7 -31
  34. package/lib/build/{provider → plugins}/java/index.js +18 -26
  35. package/lib/build/plugins/mtx/index.js +41 -0
  36. package/lib/build/plugins/mtx/resourcesTarBuilder.js +41 -0
  37. package/lib/build/{provider → plugins}/mtx-extension/extensionCompilation.js +1 -1
  38. package/lib/build/{provider → plugins}/mtx-extension/index.js +35 -44
  39. package/lib/build/{provider → plugins}/mtx-sidecar/index.js +17 -3
  40. package/lib/build/{provider → plugins}/nodejs/index.js +29 -28
  41. package/lib/build/{plugin.js → plugins/plugin.js} +45 -59
  42. package/lib/build/plugins.js +11 -8
  43. package/lib/build/util.js +0 -8
  44. package/lib/compile/to-xsuaa.js +2 -2
  45. package/lib/deploy/to-hana/hana.js +64 -56
  46. package/lib/import/common.js +1 -1
  47. package/lib/import/odata/metadataconverter/MetadataConverter.js +4 -3
  48. package/lib/import/odata/v2parser.js +28 -28
  49. package/lib/import/odata/v4parser.js +37 -2
  50. package/lib/import/openapi/index.js +1 -1
  51. package/lib/init/constants.js +0 -1
  52. package/lib/init/template/app-frontend/index.js +1 -1
  53. package/lib/init/template/approuter/files/package.json +1 -1
  54. package/lib/init/template/completion/completionSetup.js +1 -1
  55. package/lib/init/template/data/as-csv.js +1 -1
  56. package/lib/init/template/github-actions/index.js +1 -1
  57. package/lib/init/template/handler/index.js +1 -1
  58. package/lib/init/template/helm/index.js +3 -3
  59. package/lib/init/template/kyma/index.js +3 -3
  60. package/lib/init/template/lint/index.js +7 -21
  61. package/lib/init/template/multitenancy/files/package.json.hbs +0 -5
  62. package/lib/init/template/postgres/files/containerize.yaml.hbs +15 -0
  63. package/lib/init/template/postgres/files/package.json.hbs +35 -0
  64. package/lib/init/template/postgres/files/values.yaml.hbs +23 -0
  65. package/lib/init/template/postgres/index.js +34 -9
  66. package/lib/init/template/test/files/package.json +1 -1
  67. package/lib/init/template/typescript/files/package.json +1 -1
  68. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +1 -0
  69. package/lib/lint/eslintHelp.json +1 -1
  70. package/lib/lint/io.js +9 -13
  71. package/lib/mtx/pull.js +1 -1
  72. package/lib/mtx/settings_manager.js +11 -5
  73. package/lib/mtx/tenant_action.js +1 -1
  74. package/lib/mtx/util/fs.js +2 -2
  75. package/lib/util/fs.js +3 -3
  76. package/lib/util/git.js +1 -1
  77. package/lib/util/glob.js +1 -1
  78. package/lib/watch/hot-reload.js +202 -0
  79. package/lib/watch/livereload-server.js +6 -4
  80. package/lib/watch/node-watch.js +5 -1
  81. package/lib/watch/watched.js +7 -3
  82. package/lib/watch/watcher.js +41 -3
  83. package/npm-shrinkwrap.json +31 -22
  84. package/package.json +1 -1
  85. package/lib/build/buildTaskEngine.js +0 -377
  86. package/lib/build/buildTaskFactory.js +0 -198
  87. package/lib/build/buildTaskProvider.js +0 -38
  88. package/lib/build/buildTaskProviderFactory.js +0 -233
  89. package/lib/build/provider/internalBuildTaskProvider.js +0 -227
  90. package/lib/build/provider/java-cf/index.js +0 -1
  91. package/lib/build/provider/mtx/index.js +0 -36
  92. package/lib/build/provider/mtx/resourcesTarBuilder.js +0 -102
  93. package/lib/build/provider/node-cf/index.js +0 -1
  94. package/lib/init/template/lint/files/.eslint/rules/no-entity-moo.js +0 -40
  95. package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/fixed/schema.cds +0 -4
  96. package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/invalid/schema.cds +0 -4
  97. package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/rule.test.js +0 -15
  98. package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/valid/schema.cds +0 -6
  99. package/lib/init/types.d.ts +0 -23
  100. /package/lib/build/{provider → plugins}/hana/2migration.js +0 -0
  101. /package/lib/build/{provider → plugins}/hana/template/.hdiconfig-all +0 -0
  102. /package/lib/build/{provider → plugins}/hana/template/.hdiconfig-cloud +0 -0
  103. /package/lib/build/{provider → plugins}/hana/template/.hdinamespace +0 -0
  104. /package/lib/build/{provider → plugins}/hana/template/package.json +0 -0
  105. /package/lib/build/{provider → plugins}/hana/template/undeploy.json +0 -0
@@ -1,24 +1,29 @@
1
- const cds = require('../cds')
2
- const { exists, isdir } = cds.utils
3
- const BuildTaskEngine = require('./buildTaskEngine')
4
- const Plugin = require('./plugin')
5
- const { BuildError } = require('./util')
1
+ const path = require('node:path')
2
+ const { inspect } = require('node:util')
3
+ // @ts-expect-error - _log not declared in cds-types
4
+ const cds = require('../cds'), { _log: log } = cds
5
+ const { exists, isdir, rimraf, mkdirp, write, fs, colors: { YELLOW, RESET } } = cds.utils
6
+ const Plugin = require('./plugins/plugin')
7
+ const { relativePaths, BuildError, BuildMessage, resolveRequiredSapModels, normalizePath, getDefaultModelPaths, hasOptionValue } = require('./util')
8
+ const { sortMessagesSeverityAware, deduplicateMessages, CompilationError } = cds.compiler
9
+ const { SEVERITIES, SEVERITY_ERROR, LOG_LEVELS, DEFAULT_SRC_FOLDER, OPTION_WS, IGNORE_DEFAULT_MODELS, NODEJS_MODEL_EXCLUDE_LIST, BUILD_TASK_NODEJS, BUILD_TASK_NODE_CF, BUILD_TASK_JAVA_CF, BUILD_TASK_JAVA } = require("./constants")
10
+ const COMPILATION_ERROR = 'CompilationError'
11
+ const COMPILE_MESSAGE = 'CompileMessage'
12
+ const DEBUG = cds.debug('cli|build')
13
+ const LOG_LIMIT = 30
6
14
 
7
- module.exports = { build, register, Plugin, BuildError }
8
-
9
- // reuse schema definition from cds-rc.js used for package.json code completion
10
- /** @typedef{import('../env/schemas/cds-rc')['$defs']['buildTaskType']['enum'][number]} BuildFor */
11
-
12
- // using (string & {}) prevents widening of `'x' | string` to just `string`
13
15
  /**
16
+ * @typedef {import('./plugins/plugin')} Plugin_
17
+ * @typedef {import('./plugins/plugin').Task} Task
18
+ * @typedef {import('../env/schemas/cds-rc')['$defs']['buildTaskType']['enum'][number]} BuildFor
19
+ * using (string & {}) prevents widening of `'x' | string` to just `string`
14
20
  * @typedef {{
15
21
  * clean?: boolean,
16
- * outputMode?: number,
17
22
  * for?: BuildFor | (string & {}),
18
23
  * src?: string,
19
24
  * dest?: string,
20
25
  * model?: string[],
21
- * root?: string = cds.root,
26
+ * root?: string
22
27
  * } & {[key:string]:unknown}} BuildOptions
23
28
  */
24
29
 
@@ -27,23 +32,562 @@ module.exports = { build, register, Plugin, BuildError }
27
32
  *
28
33
  * @param {BuildOptions} options - command options
29
34
  */
30
- async function build({ root = cds.root, ...options } = {}) {
35
+ async function build({ root = cds.root, tasks, ...options } = {}) {
31
36
  if (!exists(root) || !isdir(root)) {
32
37
  throw `Project folder '${root}' does not exist`
33
38
  }
34
- try {
35
- return await new BuildTaskEngine(options).processTasks()
36
- } catch (e) {
37
- if (e instanceof BuildError || e.constructor.name === 'CompilationError') {
38
- cds._log(e.messages.length ? e.messages : e.message, { 'log-level': options['log-level'] || cds.env['log-level'] })
39
- console.log()
40
- // CompilationError.message also includes the detail messages - do not log twice
41
- throw e.constructor.name === 'CompilationError' ? 'CDS compilation failed' : e.messages.length ? e.message : 'CDS build failed'
42
- }
43
- throw e
44
- }
39
+
40
+ return await new BuildTaskEngine(options).processTasks(tasks)
45
41
  }
46
42
 
47
43
  function register(id, plugin) {
48
44
  return require('./plugins').register(id, plugin)
49
45
  }
46
+
47
+ class BuildTaskEngine {
48
+
49
+ /**
50
+ * @param {BuildOptions} options
51
+ */
52
+ constructor(options = {}) {
53
+ if (options.clean === undefined) {
54
+ options.clean = true
55
+ }
56
+ for (const key of ['for', 'src', 'dest']) if (options[key]) {
57
+ if (typeof options[key] !== 'string') {
58
+ throw new BuildError(`Invalid build options - property '${key}' must be a string`)
59
+ }
60
+ if (key === 'src' || key === 'dest') {
61
+ // normalize path separator and remove trailing slash
62
+ options[key] = normalizePath(options[key])
63
+ }
64
+ }
65
+ /** @type {{ options: BuildOptions, tasks: Task[]}} */
66
+ this._context = { options, tasks: [] } // resolved tasks
67
+ const plugins = require('./plugins')
68
+ this._plugins = plugins.all
69
+ plugins.registerDefaults()
70
+ }
71
+
72
+ get options() {
73
+ return this._context.options
74
+ }
75
+ get context() {
76
+ return this._context
77
+ }
78
+ get plugins() {
79
+ return this._plugins
80
+ }
81
+
82
+ async processTasks(tasks) {
83
+ const startTime = Date.now()
84
+
85
+ tasks ??= await this.getTasks()
86
+ console.log(`building project with`, inspect({
87
+ versions: { cds: cds.version, compiler: cds.compiler.version(), dk: require('../../package.json').version },
88
+ target: cds.env.build.target,
89
+ tasks,
90
+ ...(!this.options.clean && { clean: this.options.clean }),
91
+ }, { depth: 11, colors: cds.utils.colors.enabled, compact: 3, breakLength: 250 }))
92
+
93
+ // validate required @sap namespace models - log only
94
+ const unresolved = BuildTaskEngine._resolveRequiredSapServices(tasks)
95
+ if (unresolved.length > 0) {
96
+ throw new BuildError(`Required CDS models [${unresolved.join(', ')}] cannot be resolved. Run the 'npm install' command to install up-to-date versions of the missing packages.`)
97
+ }
98
+
99
+ // create build plugins
100
+ const plugins = []
101
+ tasks.forEach((task) => {
102
+ if (task) {
103
+ const plugin = this.createPlugin(task)
104
+ plugin.init()
105
+ plugins.push(plugin)
106
+ }
107
+ })
108
+
109
+ try {
110
+ await this._executeCleanBuildTasks(plugins)
111
+
112
+ // throwing Exception in case of compilation errors
113
+ const buildResult = await this._executeBuildTasks(plugins)
114
+
115
+ await this._writeGenerationLog(plugins)
116
+ this._logOutput(plugins)
117
+ this._logMessages(plugins.reduce((acc, plugin) => acc.concat(plugin.messages), []))
118
+ console.log(`build completed in ${Date.now() - startTime} ms`)
119
+
120
+ return buildResult
121
+ } catch (error) {
122
+ this._logOutput(plugins)
123
+ throw error
124
+ }
125
+ }
126
+
127
+ async getTasks() {
128
+ const tasks = await this._createTasks()
129
+ const resolvedTasks = this.resolveTasks(tasks)
130
+ if (this.options.resolve) return resolvedTasks
131
+ return tasks
132
+ }
133
+
134
+ /**
135
+ * Create a Plugin instance for the given build task.
136
+ * @param {Task} task
137
+ */
138
+ createPlugin(task) {
139
+ const PluginClass = this.getPlugin(task)
140
+ const resolvedTask = this._resolveTask(task)
141
+ DEBUG?.(`loaded build plugin [${resolvedTask.for}]`)
142
+
143
+ const plugin = new PluginClass()
144
+ if (!(plugin instanceof PluginClass)) {
145
+ throw new Error(`Invalid Build plugin type ${task.for}`)
146
+ }
147
+ plugin._task = resolvedTask
148
+ plugin._context = this.context
149
+ this.context.tasks.push(resolvedTask)
150
+ DEBUG?.(`created build plugin [${resolvedTask.for}]`)
151
+ return plugin
152
+ }
153
+
154
+ async _createTasks() {
155
+ DEBUG?.(`determining build tasks for project [${cds.root}].`)
156
+
157
+ let tasks = Array.isArray(cds.env.build?.tasks) ? cds.clone(cds.env.build.tasks) : []
158
+ if (tasks.length === 0) {
159
+ tasks = await this.lookupTasks()
160
+ this._applyCliOptions(tasks)
161
+ } else {
162
+ this._applyCliOptions(tasks)
163
+ // 1. apply default values including task.for and ensure that for all tasks a provider exists - throwing error otherwise
164
+ await this.applyTaskDefaults(tasks)
165
+ // ensure that dependencies get wired up before filtering
166
+ await this.lookupTasks(tasks, true)
167
+ }
168
+
169
+ // 2. filters the list of build tasks
170
+ let existingTasks = tasks
171
+ tasks = await this._filterTasksForCli(tasks)
172
+ if (tasks.length === 0) return tasks
173
+
174
+ // 3. add dependencies
175
+ existingTasks = [...tasks]
176
+ await this.lookupTasks(tasks, true)
177
+ if (tasks.length > existingTasks.length) {
178
+ const newTasks = tasks.filter(task => !existingTasks.includes(task))
179
+ this._applyCliOptions(newTasks)
180
+ }
181
+
182
+ await BuildTaskEngine._applyCommonTaskDefaults(tasks)
183
+
184
+ // Java projects use "." as the default build target folder
185
+ if (cds.env['project-nature'] === 'java' && cds.env.build.target !== ".") {
186
+ const userEnv = cds.env.for("cds", cds.root, false)
187
+ if (!userEnv.build?.target) {
188
+ cds.env.build.target = "."
189
+ DEBUG?.("using inplace build for java project instead of default staging build")
190
+ }
191
+ }
192
+ return tasks
193
+ }
194
+
195
+ static async _applyCommonTaskDefaults(tasks) {
196
+ tasks.forEach(task => {
197
+ if (task.options?.model && !Array.isArray(task.options.model)) {
198
+ task.options.model = [task.options.model]
199
+ }
200
+ })
201
+ const modelPaths = await getDefaultModelPaths(false)
202
+ let wsModelPaths
203
+ if (cds.cli.options?.[OPTION_WS] || tasks.some(task => hasOptionValue(task.options?.[OPTION_WS], true))) {
204
+ wsModelPaths = await getDefaultModelPaths(true)
205
+ }
206
+ tasks.forEach(task => {
207
+ if (!task.src) {
208
+ throw new BuildError(`Mandatory property 'src' not defined for build task '${task.for}'.`)
209
+ }
210
+ const optionWs = cds.cli.options?.[OPTION_WS] || hasOptionValue(task.options?.[OPTION_WS], true)
211
+ this._setTaskModelOptions(task, optionWs ? [...wsModelPaths] : [...modelPaths])
212
+ })
213
+ }
214
+
215
+ async _filterTasksForCli(tasks) {
216
+ const options = this.options
217
+ let resultTasks = tasks.filter(task => {
218
+ return (!options.for || options.for === task.for)
219
+ && (!options.src || options.src === task.src)
220
+ })
221
+ if (resultTasks.length === 0) {
222
+ if (options.for) {
223
+ const task = this.getTask({ for: options.for })
224
+ if (options.src) task.src = options.src
225
+ resultTasks.push(task)
226
+ this._applyCliOptions(resultTasks)
227
+ await this.applyTaskDefaults(resultTasks)
228
+ }
229
+ } else if (resultTasks.length <= tasks.length) {
230
+ tasks.length = 0
231
+ resultTasks.forEach(task => tasks.push(task))
232
+ resultTasks = tasks
233
+ }
234
+ return resultTasks
235
+ }
236
+
237
+ _applyCliOptions(tasks) {
238
+ const options = this.options
239
+ tasks.forEach(task => {
240
+ if (options.dest) task.dest = options.dest
241
+ if (options.taskOptions) {
242
+ const taskOptions = cds.clone(options.taskOptions)
243
+ task.options = task.options ? Object.assign(task.options, taskOptions) : taskOptions
244
+ }
245
+ })
246
+ }
247
+
248
+ static escapeRegExpInput(str) {
249
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
250
+ }
251
+
252
+ static _setTaskModelOptions(task, defaultModelPaths) {
253
+ if (task.for !== BUILD_TASK_NODEJS && task.for !== BUILD_TASK_NODE_CF) {
254
+ defaultModelPaths = defaultModelPaths.filter(p => !NODEJS_MODEL_EXCLUDE_LIST.includes(p))
255
+ }
256
+
257
+ let taskModelPaths = task.options?.model
258
+ if (taskModelPaths && !Array.isArray(taskModelPaths)) {
259
+ taskModelPaths = [taskModelPaths]
260
+ }
261
+ const allowList = new RegExp(`^@sap/cds${cds.env.requires.toggles
262
+ ? '|^' + BuildTaskEngine.escapeRegExpInput(cds.env.features.folders.replace('*', ''))
263
+ : ''}`)
264
+ task.options = task.options || {}
265
+ let modelPaths = []
266
+ if (taskModelPaths?.length) {
267
+ if (!hasOptionValue(task.options?.[IGNORE_DEFAULT_MODELS], true)) {
268
+ modelPaths = defaultModelPaths.filter(p => p.match(allowList))
269
+ }
270
+ } else {
271
+ modelPaths = [...defaultModelPaths]
272
+ if (!modelPaths.includes(task.src)) modelPaths.push(task.src)
273
+ if (hasOptionValue(task.options?.[IGNORE_DEFAULT_MODELS], true)) {
274
+ modelPaths = modelPaths.filter(p => !p.match(allowList))
275
+ }
276
+ }
277
+ task.options.model = [...new Set(taskModelPaths?.length ? taskModelPaths.concat(modelPaths) : modelPaths)]
278
+ }
279
+
280
+ getTask(key) {
281
+ this._normalizeKey(key)
282
+ this._assertProvided(key)
283
+ const task = cds.clone(this.plugins.get(key.for)?.taskDefaults ?? {})
284
+ task.for = key.for
285
+ task.src ??= DEFAULT_SRC_FOLDER
286
+ task.src = task.src.replace(/\/$/, '')
287
+ return task
288
+ }
289
+
290
+ async applyTaskDefaults(tasks) {
291
+ return Promise.all(tasks.map(async (task) => {
292
+ if (!task.for) throw new BuildError(`Mandatory property 'for' not defined for build task.`)
293
+ return this._applyTaskDefaults(task)
294
+ }))
295
+ }
296
+
297
+ async lookupTasks(tasks = [], dependencies) {
298
+ const existingTasks = [...tasks]
299
+
300
+ if (!dependencies) {
301
+ for (const [id, PluginClass] of this.plugins) {
302
+ if (PluginClass.hasTask()) tasks.push(this.getTask({ for: id }))
303
+ }
304
+ } else {
305
+ const toAdd = []
306
+ for (const task of tasks) {
307
+ const PluginClass = this.getPlugin(task)
308
+ const reqs = new PluginClass().requires || []
309
+ for (const req of reqs) {
310
+ const reqId = this._normalizeKey({ for: req }).for
311
+ const ReqPluginClass = this.plugins.get(reqId)
312
+ if (ReqPluginClass && !tasks.some(t => t.for === reqId) && !toAdd.some(t => t.for === reqId)) {
313
+ const defaultTaskSrc = this.getTask({ for: reqId }).src
314
+ const configuredTasks = cds.env.build?.tasks?.filter(t => t.for === reqId)
315
+ let configuredTask
316
+ if (configuredTasks?.length) {
317
+ configuredTask = configuredTasks.find(t => !t.src || t.src === defaultTaskSrc) || configuredTasks[0]
318
+ }
319
+
320
+ if (configuredTask) {
321
+ configuredTask = cds.clone(configuredTask)
322
+ await this._applyTaskDefaults(configuredTask)
323
+ toAdd.push(configuredTask)
324
+ } else if (ReqPluginClass.hasTask?.()) {
325
+ toAdd.push(this.getTask({ for: reqId }))
326
+ }
327
+ }
328
+ }
329
+ }
330
+ tasks.push(...toAdd)
331
+ }
332
+
333
+ if (existingTasks.length < tasks.length) {
334
+ const newTasks = tasks.filter(task => !existingTasks.includes(task))
335
+ await Promise.all(newTasks.map(t => this._applyTaskDefaults(t)))
336
+ DEBUG?.(`Build task provider ${this.constructor.name} returned build tasks ${JSON.stringify(newTasks)}`)
337
+ }
338
+ return tasks
339
+ }
340
+
341
+ /**
342
+ * Loads the build plugin implementation for the given build task.
343
+ * @param {Task} task
344
+ * @returns {typeof import('./plugins/plugin')}
345
+ */
346
+ getPlugin(task) {
347
+ this._normalizeKey(task)
348
+ this._assertProvided(task)
349
+ try {
350
+ return this.plugins.get(task.for)
351
+ } catch (e) {
352
+ console.error(`Provider failed to load build plugin class ${task.for} - provider: ${this.constructor.name}`)
353
+ throw e
354
+ }
355
+ }
356
+
357
+ /**
358
+ * @param {Task[]} tasks
359
+ */
360
+ resolveTasks(tasks) {
361
+ return tasks.map(task => this._resolveTask(task))
362
+ }
363
+
364
+ /**
365
+ * @param {Task} task
366
+ */
367
+ _resolveTask(task) {
368
+ const resolvedTask = cds.clone(task)
369
+ resolvedTask.src = path.resolve(cds.root, task.src)
370
+ if (!this.options.ws) {
371
+ try {
372
+ fs.realpathSync(resolvedTask.src)
373
+ } catch {
374
+ throw new BuildError(`The 'src' folder '${path.resolve(cds.root, task.src)}' for build task '${resolvedTask.for}' does not exist`)
375
+ }
376
+ }
377
+ resolvedTask.dest = path.resolve(cds.root, cds.env.build.target, task.dest || task.src)
378
+ resolvedTask.options = task.options || {}
379
+ return resolvedTask
380
+ }
381
+
382
+ _normalizeKey(key) {
383
+ const compat = { [BUILD_TASK_NODE_CF]: BUILD_TASK_NODEJS, [BUILD_TASK_JAVA_CF]: BUILD_TASK_JAVA }
384
+ if (compat[key.for]) {
385
+ console.warn(`${YELLOW}Build task '${key.for}' is deprecated, use '${compat[key.for]}' instead${RESET}`)
386
+ key.for = compat[key.for]
387
+ }
388
+ return key
389
+ }
390
+
391
+ _assertProvided(key) {
392
+ if (!this.plugins.has(key.for)) {
393
+ throw new BuildError(`No provider found for build task '${key.for}'. Ensure that all required dependencies have been added and 'npm install' has been executed.`)
394
+ }
395
+ }
396
+
397
+ async _applyTaskDefaults(task) {
398
+ const defaultTask = this.getTask(task)
399
+ const names = Object.getOwnPropertyNames(defaultTask)
400
+ names.forEach(name => {
401
+ task[name] ??= defaultTask[name]
402
+ })
403
+ }
404
+
405
+ async _executeCleanBuildTasks(plugins) {
406
+ if (!this.options.clean) return
407
+
408
+ if (cds.env.build.target !== '.') {
409
+ const target = path.resolve(cds.root, cds.env.build.target)
410
+ DEBUG?.(`cleaning staging folder ${target}`)
411
+ await rimraf(target)
412
+ }
413
+
414
+ const results = await Promise.allSettled(plugins.map((plugin) => {
415
+ DEBUG?.(`cleaning, plugin [${plugin.constructor.name}], src [${relativePaths(cds.root, plugin.task.src)}]`)
416
+ return plugin.clean()
417
+ }))
418
+ this._resolveHandlerResponse(results)
419
+ }
420
+
421
+ async _executeBuildTasks(plugins) {
422
+ const levels = new Map()
423
+ const getLevel = (PluginClass) => {
424
+ if (levels.has(PluginClass)) return levels.get(PluginClass)
425
+ const plugin = new PluginClass()
426
+ if (plugin.priority && plugin.priority < 0) { // cds10: compat for cds-plugin-ui5 using undocumented priority
427
+ const level = Number.MAX_SAFE_INTEGER
428
+ levels.set(PluginClass, level)
429
+ return level
430
+ }
431
+ let maxDepLevel = -1
432
+ const reqs = plugin.requires || []
433
+ for (const req of reqs) {
434
+ const reqId = this._normalizeKey({ for: req }).for
435
+ const ReqPluginClass = this.plugins.get(reqId)
436
+ if (ReqPluginClass) maxDepLevel = Math.max(maxDepLevel, getLevel(ReqPluginClass))
437
+ }
438
+ const level = maxDepLevel + 1
439
+ levels.set(PluginClass, level)
440
+ return level
441
+ }
442
+
443
+ plugins.forEach(p => getLevel(p.constructor))
444
+
445
+ const groupsMap = new Map()
446
+ plugins.forEach(p => {
447
+ const lvl = levels.get(p.constructor)
448
+ if (!groupsMap.has(lvl)) groupsMap.set(lvl, [])
449
+ groupsMap.get(lvl).push(p)
450
+ })
451
+
452
+ const sortedLevels = [...groupsMap.keys()].sort((a, b) => a - b)
453
+ const pipeline = sortedLevels.map(lvl => groupsMap.get(lvl))
454
+
455
+ const results = await this._executePipeline(pipeline)
456
+ return this._resolveHandlerResponse(results, plugins.flatMap(plugin => plugin.messages))
457
+ }
458
+
459
+ async _executePipeline(pipeline) {
460
+ let allResults = []
461
+ for (const group of pipeline) {
462
+ const results = await Promise.allSettled(group.map((plugin) => {
463
+ DEBUG?.(`building, plugin [${plugin.constructor.name}], src [${relativePaths(cds.root, plugin.task.src)}]`)
464
+ return plugin.build().then(pluginResult => Promise.resolve({
465
+ task: plugin.task,
466
+ result: pluginResult,
467
+ messages: this._sortMessagesUnique(plugin.messages)
468
+ }))
469
+ }))
470
+ allResults = allResults.concat(results)
471
+ }
472
+ return allResults
473
+ }
474
+
475
+ _resolveHandlerResponse(results, pluginMessages = []) {
476
+ /** @type {(BuildError | Error)[]}*/
477
+ const errors = []
478
+ const resolvedResults = results.reduce((acc, r) => {
479
+ if (r.status === 'fulfilled') acc.push(r.value)
480
+ if (r.status === 'rejected' && r.reason) errors.push(r.reason)
481
+ return acc
482
+ }, [])
483
+
484
+ if (errors.length > 0) {
485
+ const error = errors.find(e => e.constructor.name !== COMPILATION_ERROR)
486
+ if (error) {
487
+ if (!(error instanceof BuildError)) throw error
488
+ error.messages = error.messages?.map(m => typeof m === 'string' ? new BuildMessage(m, SEVERITY_ERROR) : m)
489
+ error.messages = this._sortMessagesUnique(pluginMessages.filter(m => m instanceof BuildMessage), error.messages)
490
+ throw error
491
+ }
492
+
493
+ const compileErrors = errors.filter(e => e.constructor.name === COMPILATION_ERROR)
494
+ const compileMessages = pluginMessages.filter(message => message.constructor.name === COMPILE_MESSAGE)
495
+ if (compileErrors.length) {
496
+ throw new CompilationError(this._sortMessagesUnique(BuildTaskEngine._getErrorMessages(compileErrors), compileMessages))
497
+ }
498
+ }
499
+ return resolvedResults
500
+ }
501
+
502
+ _logOutput(plugins) {
503
+ const files = BuildTaskEngine._getOutput(plugins)
504
+ if (files.length > 0) {
505
+ const term = require('../util/term')
506
+ if (!process.env.DEBUG && files.length > LOG_LIMIT) {
507
+ const length = files.length
508
+ files.length = LOG_LIMIT
509
+ files.push(`... ${length - files.length} more. Run with DEBUG=build to show all files.`)
510
+ }
511
+ console.log(`done > wrote output to:\n ${term.dim(files.join("\n "))}\n`)
512
+ }
513
+ }
514
+
515
+ async _writeGenerationLog(plugins) {
516
+ const outputFile = cds.env.build.outputfile || process.env.GENERATION_LOG
517
+ if (outputFile) {
518
+ const files = BuildTaskEngine._getOutput(plugins)
519
+ console.log(`writing generation log to [${outputFile}]\n`)
520
+ try {
521
+ await mkdirp(path.dirname(outputFile))
522
+ await write(outputFile, files.join('\n'))
523
+ } catch (error) {
524
+ console.error(`failed to write generation log`)
525
+ console.error(error.stack || error)
526
+ }
527
+ }
528
+ }
529
+
530
+ static _getOutput(plugins) {
531
+ const files = plugins.flatMap(plugin => plugin.files).sort()
532
+ return files.map(file => {
533
+ if (path.isAbsolute(cds.env.build.target)) return file
534
+ return path.relative(cds.root, file)
535
+ })
536
+ }
537
+
538
+ _logMessages(messages) {
539
+ if (messages.length > 0) {
540
+ const options = {
541
+ log: console.log,
542
+ "log-level": this._getLogLevel()
543
+ }
544
+ deduplicateMessages(messages)
545
+ sortMessagesSeverityAware(messages)
546
+ log(messages, options)
547
+ }
548
+ }
549
+
550
+ static _resolveRequiredSapServices(tasks) {
551
+ const taskModelPaths = tasks.reduce((acc, task) => {
552
+ const model = task.options?.model
553
+ if (model) {
554
+ if (Array.isArray(model)) model.forEach(m => acc.add(m))
555
+ else acc.add(model)
556
+ }
557
+ return acc
558
+ }, new Set())
559
+
560
+ return resolveRequiredSapModels([...taskModelPaths])
561
+ }
562
+
563
+ /**
564
+ * @param {(string | BuildError)[]} errors
565
+ */
566
+ static _getErrorMessages(errors) {
567
+ let messages = []
568
+ errors.forEach(error => {
569
+ if (typeof error !== 'string' && Array.isArray(error.errors) && error.errors.length > 0) {
570
+ messages = messages.concat(this._getErrorMessages(error.errors))
571
+ } else {
572
+ messages.push(error)
573
+ }
574
+ })
575
+ return messages
576
+ }
577
+
578
+ _sortMessagesUnique(...messages) {
579
+ const logLevelIdx = LOG_LEVELS.indexOf(this._getLogLevel())
580
+ messages = messages.reduce((acc, m) => acc.concat(m), [])
581
+ const filteredMessages = messages.filter(message => message && (!message.severity || logLevelIdx >= SEVERITIES.indexOf(message.severity)))
582
+ // @ts-expect-error - formally expects CompileMessage, but actually, an .severity is the only expected property
583
+ deduplicateMessages(filteredMessages)
584
+ // @ts-expect-error - see above
585
+ return sortMessagesSeverityAware(filteredMessages)
586
+ }
587
+
588
+ _getLogLevel() {
589
+ return this.options["log-level"] || cds.env["log-level"]
590
+ }
591
+ }
592
+
593
+ module.exports = { build, register, Plugin, BuildError, BuildTaskEngine }
@@ -1,11 +1,11 @@
1
- const path = require('path')
1
+ const path = require('node:path')
2
2
  const cds = require('../../cds')
3
- const FeatureToggleBuildPlugin = require('./featureToggleBuildPlugin')
4
- const { OUTPUT_MODE, OUTPUT_MODE_RESULT, FLAVOR_LOCALIZED_EDMX } = require('../constants')
3
+ const { FLAVOR_LOCALIZED_EDMX } = require('../constants')
5
4
  const DEBUG = cds.debug('cli|build')
6
5
 
7
- class EdmxBuildPlugin extends FeatureToggleBuildPlugin {
8
- async compileToEdmx(model, edmxDest, compileOptions = {}) { // NOSONAR
6
+ module.exports = class EdmxBuildPlugin extends require('./featureToggles') {
7
+
8
+ async compileToEdmx(model, edmxDest, compileOptions = {}) {
9
9
  const promises = []
10
10
 
11
11
  // generate edmx for services only that have the odata protocol
@@ -34,9 +34,6 @@ class EdmxBuildPlugin extends FeatureToggleBuildPlugin {
34
34
  }
35
35
  for (let [content, key] of result) {
36
36
  const serviceName = key.file ? key.file : key.name
37
- if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
38
- this._result.services.add(serviceName)
39
- }
40
37
  const locResult = cds.localize(model, langs, content)
41
38
  if (locResult[Symbol.iterator]) { // multi result
42
39
  for (let [localizedContent, { lang }] of locResult) {
@@ -65,16 +62,10 @@ class EdmxBuildPlugin extends FeatureToggleBuildPlugin {
65
62
 
66
63
  _writeEdmxForLang(content, serviceName, lang, edmxDest) {
67
64
  const fileName = serviceName + (lang ? '_' + lang + '.xml' : '.xml')
68
- if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_RESULT)) {
69
- this._result.languages.add(lang)
70
- this._result.edmx.set(fileName, content)
71
- }
72
-
73
- //edmxDest might be null
65
+ if (this._cachedEdmx) this._cachedEdmx.set(fileName, content)
74
66
  if (edmxDest) {
75
67
  return this.write(content).to(path.join(edmxDest, fileName))
76
68
  }
77
69
  return Promise.resolve()
78
70
  }
79
71
  }
80
- module.exports = EdmxBuildPlugin