@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
@@ -80,6 +80,7 @@ resources:
80
80
  ProxyType: Internet
81
81
  HTML5.ForwardAuthToken: true
82
82
  HTML5.DynamicDestination: true
83
+ {{#hasIas}}HTML5.IASDependencyName: {{appName}}-ias-api{{/hasIas}}
83
84
  requires:
84
85
  - name: srv-api
85
86
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "9.39.2",
2
+ "version": "9.39.3",
3
3
  "help": "\n# SYNOPSIS\n\n *cds lint* [options] file.js [file.js] [dir]\n\n Runs environment checks and/or checks the specified models\n based on the ESLint framework\n\n# OPTIONS\n \n Basic configuration:\n --no-config-lookup Disable look up for eslint.config.js\n -c, --config path::String Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs\n --inspect-config Open the config inspector with the current configuration\n --ext [String] Specify additional file extensions to lint\n --global [String] Define global variables\n --parser String Specify the parser to be used\n --parser-options Object Specify parser options\n \n Specify Rules and Plugins:\n --plugin [String] Specify plugins\n --rule Object Specify rules\n \n Fix Problems:\n --fix Automatically fix problems\n --fix-dry-run Automatically fix problems without saving the changes to the file system\n --fix-type Array Specify the types of fixes to apply (directive, problem, suggestion, layout)\n \n Ignore Files:\n --no-ignore Disable use of ignore files and patterns\n --ignore-pattern [String] Patterns of files to ignore\n \n Use stdin:\n --stdin Lint code provided on <STDIN> - default: false\n --stdin-filename String Specify filename to process STDIN as\n \n Handle Warnings:\n --quiet Report errors only - default: false\n --max-warnings Int Number of warnings to trigger nonzero exit code - default: -1\n \n Output:\n -o, --output-file path::String Specify file to write report to\n -f, --format String Use a specific output format - default: stylish\n --color, --no-color Force enabling/disabling of color\n \n Inline configuration comments:\n --no-inline-config Prevent comments from changing config or rules\n --report-unused-disable-directives Adds reported errors for unused eslint-disable and eslint-enable directives\n --report-unused-disable-directives-severity String Chooses severity level for reporting unused eslint-disable and eslint-enable directives - either: off, warn, error, 0, 1, or 2\n --report-unused-inline-configs String Adds reported errors for unused eslint inline config comments - either: off, warn, error, 0, 1, or 2\n \n Caching:\n --cache Only check changed files - default: false\n --cache-file path::String Path to the cache file. Deprecated: use --cache-location - default: .eslintcache\n --cache-location path::String Path to the cache file or directory\n --cache-strategy String Strategy to use for detecting changed files in the cache - either: metadata or content - default: metadata\n \n Suppressing Violations:\n --suppress-all Suppress all violations - default: false\n --suppress-rule [String] Suppress specific rules\n --suppressions-location path::String Specify the location of the suppressions file\n --prune-suppressions Prune unused suppressions - default: false\n --pass-on-unpruned-suppressions Ignore unused suppressions - default: false\n \n Miscellaneous:\n --init Run config initialization wizard - default: false\n --env-info Output execution environment information - default: false\n --no-error-on-unmatched-pattern Prevent errors when pattern is unmatched\n --exit-on-fatal-error Exit with exit code 2 in case of fatal error - default: false\n --no-warn-ignored Suppress warnings when the file list includes ignored files\n --pass-on-no-patterns Exit with exit code 0 in case no file patterns are passed\n --debug Output debugging information\n -h, --help Show help\n -v, --version Output the version number\n --print-config path::String Print the configuration for the given file\n --stats Add statistics to the lint report - default: false\n --flag [String] Enable a feature flag\n --mcp Start the ESLint MCP server\n --concurrency Int|String Number of linting threads, auto to choose automatically, off for no multithreading - default: off\n \n",
4
4
  "options": [
5
5
  "--config",
package/lib/lint/io.js CHANGED
@@ -30,7 +30,7 @@ module.exports = {
30
30
  switch (path.basename(configPath)) {
31
31
  case 'eslint.config.mjs':
32
32
  // pathToFileURL is used to convert the path to a file URL on Windows
33
- try { config = (await import (pathToFileURL(configPath))).default } catch(e) { config = null }
33
+ try { config = (await import (pathToFileURL(configPath))).default } catch { config = null }
34
34
  break
35
35
  case 'eslint.config.js':
36
36
  case 'eslint.config.cjs':
@@ -44,9 +44,9 @@ module.exports = {
44
44
  * plugin with:
45
45
  * (1) All CDS recommended rules 'on'
46
46
  * (2) Add's custom rule example 'no-entity-moo' if requested
47
- * @param {object | string} configPath - ESLint config file or path
47
+ * @param {object | string} configIn - ESLint config file or path
48
48
  * @param {import('console').Console} logger - logger to use
49
- * @returns {object}
49
+ * @returns {Promise<object>} ESLint config contents (also writes to file if path provided)
50
50
  */
51
51
  async sanitizeEslintConfig(configIn, logger = console) {
52
52
  let configContents
@@ -64,8 +64,9 @@ module.exports = {
64
64
 
65
65
  /**
66
66
  * Suggest ESLint config contents to file
67
- * @param {*} configContents config file contents
68
67
  * @param {import('node:fs').PathLike} configPath config file path to write to
68
+ * @param {boolean} force whether to overwrite existing config file or not
69
+ * @param {import('console').Console} logger logger to use for output
69
70
  * @returns {Promise<void>}
70
71
  */
71
72
  async writeEslintConfig(configPath, force = false, logger = console) {
@@ -142,21 +143,16 @@ export default [...cds.recommended, cdsPlugin.configs.recommended]
142
143
  * (2) 'Custom' case adds rulePaths and removes configFile
143
144
  * (3) 'Global' case adds configFile and removes rulePaths
144
145
  * @param {*} settingsPath VS Code settings file
146
+ * @param {*} lintFileTypes array of file types to add to eslint.validate setting
147
+ * @returns void
145
148
  */
146
- async sanitizeVscodeSettings(settingsPath, lintFileTypes, customRuleExample = false) {
149
+ async sanitizeVscodeSettings(settingsPath, lintFileTypes) {
147
150
  const settings = (await this.readVscodeSettings(settingsPath)) ?? {}
148
- const rulePaths = [path.join('.eslint/rules')]
149
151
 
150
152
  settings['eslint.validate'] = settings['eslint.validate']
151
153
  ? this.mergeArrays(settings['eslint.validate'], lintFileTypes)
152
154
  : lintFileTypes
153
155
 
154
- if (settings['eslint.options'] && customRuleExample) {
155
- settings['eslint.options'].rulePaths = rulePaths
156
- } else if (customRuleExample) {
157
- settings['eslint.options'] = { rulePaths: rulePaths }
158
- }
159
-
160
156
  await write(settingsPath, settings, { spaces: 2 })
161
- },
157
+ }
162
158
  }
package/lib/mtx/pull.js CHANGED
@@ -111,7 +111,7 @@ module.exports = class Pull {
111
111
  .find(ws => {
112
112
  try {
113
113
  return require(join(this.projectFolder, ws, 'package.json')).name === this.baseModelName;
114
- } catch (e) { /* ignore */ }
114
+ } catch { /* ignore */ }
115
115
  }) ?? defaultFolder;
116
116
  }
117
117
  } else {
@@ -288,11 +288,17 @@ class SettingsManager {
288
288
  async function updateTokenUrl() {
289
289
  const tokenUrl = secure(params.get('tokenUrl'), 'token URL');
290
290
  const renewUrl = !logout && !params.get('skipToken')
291
- && (!tokenUrl || tokenUrl.includes('//-/cds') || tokenUrl.includes(OAUTH_PATH_LEGACY) || params.get('appUrl') !== loadedSettings.get('appUrl'));
291
+ && (!tokenUrl || tokenUrl.includes('//-/cds') || tokenUrl.includes(OAUTH_PATH_LEGACY)
292
+ || params.get('appUrl') !== loadedSettings.get('appUrl')
293
+ || params.get('subdomain') !== loadedSettings.get('subdomain'));
292
294
  if (renewUrl) {
293
295
  const tokenUrl = await getTokenUrl(params, renewUrl);
294
296
  params.set('tokenUrl', tokenUrl);
295
297
  DEBUG?.(`Updated token URL from '${loadedSettings.get('tokenUrl')}' (loaded) to '${tokenUrl}'`);
298
+ if (params.has('passcodeUrl')) {
299
+ params.delete('passcodeUrl');
300
+ DEBUG?.('Cleared stale passcode URL due to token URL renewal');
301
+ }
296
302
  }
297
303
  }
298
304
 
@@ -598,7 +604,7 @@ class SettingsManager {
598
604
  let settingsByFolder
599
605
  try {
600
606
  settingsByFolder = JSON.parse((await fs.promises.readFile(CONFIG.paths.settings)).toString());
601
- } catch (err) {
607
+ } catch {
602
608
  DEBUG?.('Empty settings file');
603
609
  return emptyData(all);
604
610
  }
@@ -626,7 +632,7 @@ class SettingsManager {
626
632
  let allAuth;
627
633
  try {
628
634
  allAuth = JSON.parse((await fs.promises.readFile(CONFIG.paths.auth)).toString());
629
- } catch (err) {
635
+ } catch {
630
636
  DEBUG?.(`${capitalize(TOKEN_STORAGE_DESC.plain)} contains invalid saved auth`);
631
637
  return emptyData(all);
632
638
  }
@@ -670,7 +676,7 @@ class SettingsManager {
670
676
 
671
677
  try {
672
678
  auth = JSON.parse(authString);
673
- } catch (error) {
679
+ } catch {
674
680
  auth = {};
675
681
  }
676
682
 
@@ -693,7 +699,7 @@ class SettingsManager {
693
699
  if (appUrl && subdomain) {
694
700
  try {
695
701
  (result[appUrl] ??= {})[subdomain] = JSON.parse(authString);
696
- } catch (error) {
702
+ } catch {
697
703
  throw new Error(`${capitalize(TOKEN_STORAGE_DESC.keyring)} contains invalid saved auth for URL ${appUrl} and subdomain ${subdomain}`);
698
704
  }
699
705
  }
@@ -81,7 +81,7 @@ module.exports = class TenantAction {
81
81
  try {
82
82
  libPath = require.resolve(libId, { paths: [process.cwd()] });
83
83
  DEBUG?.(`Using local @sap/cds-mtxs from ${libPath}`);
84
- } catch (error) {
84
+ } catch {
85
85
  try {
86
86
  libPath = require.resolve(libId);
87
87
  DEBUG?.(`Using cds-mtxs from ${libPath}`);
@@ -42,7 +42,7 @@ function _traverseFileSystem(root, sourceDir, files, filter) {
42
42
  files.set(toUnixPath(path.relative(root, entry)), fileContent.toString('utf-8'));
43
43
  }
44
44
  });
45
- } catch(error) { /* ignored */}
45
+ } catch { /* ignored */}
46
46
  }
47
47
 
48
48
  /**
@@ -87,7 +87,7 @@ function grepQR(folder, extensions, regex) {
87
87
  if (regex.test(fs.readFileSync(path.join(folder, dirent.name), { encoding: 'utf-8' }))) {
88
88
  return true;
89
89
  }
90
- } catch (error) { /* ignore */ }
90
+ } catch { /* ignore */ }
91
91
  }
92
92
  if (dirent.isDirectory() && grepQR(path.join(folder, dirent.name), extensions, regex)) {
93
93
  return true;
package/lib/util/fs.js CHANGED
@@ -45,7 +45,7 @@ async function readJSON(src, project) {
45
45
  try {
46
46
  return JSON.parse(file)
47
47
  } catch (error) {
48
- throw new Error(error.message + '\n' + file)
48
+ throw new Error(`${error.message}\n${file}`, { cause: error })
49
49
  }
50
50
  }
51
51
 
@@ -82,8 +82,8 @@ const DEFAULT_CONCURRENCY = (() => {
82
82
  * @returns {Promise<string[]>} Array of matching file paths
83
83
  */
84
84
  async function find(dir, { filter, ignoreSymlinks = false } = {}) {
85
- const files = /** @type {string[]} */([])
86
- const queue = /** @type {string[]} */([dir])
85
+ const files = /** @type {import('fs').Dirent[]} */([])
86
+ const queue = /** @type {import('fs').Dirent[]} */([dir])
87
87
  const active = new Set()
88
88
  const visit = async (/** @type {string} */ d) => {
89
89
  let entries = /** @type {import('fs').Dirent[]} */([])
package/lib/util/git.js CHANGED
@@ -46,7 +46,7 @@ class GitUtil {
46
46
  async readFileSafely(file) {
47
47
  try {
48
48
  return await fs.readFile(file, 'utf8');
49
- } catch (err) {
49
+ } catch {
50
50
  return '';
51
51
  }
52
52
  }
package/lib/util/glob.js CHANGED
@@ -50,7 +50,7 @@ async function subdirs(dir) {
50
50
  let entries;
51
51
  try {
52
52
  entries = await readdir(dir, { withFileTypes: true });
53
- } catch (err) {
53
+ } catch {
54
54
  return [];
55
55
  }
56
56
 
@@ -0,0 +1,202 @@
1
+ const crypto = require('crypto')
2
+ const stream = require('stream/promises')
3
+ const cds = require('../cds');
4
+ const log = cds.log('cds');
5
+
6
+ module.exports = async function hotReloading() {
7
+ const csn = await cds.compile('*');
8
+ const sqliteModel = cds.compile(csn).to.sql({ dialect: 'sqlite' })
9
+ const hash = crypto.createHash('sha256')
10
+ await stream.pipeline(sqliteModel, hash)
11
+ cds.model.$schemaHash = hash.digest('hex')
12
+ if (cds.env.requires.db.impl === '@cap-js/sqlite') {
13
+ cds.model.$schema = sqliteModel
14
+ }
15
+ process.on('message', async (msg) => {
16
+ if (msg.code === 'refreshModel') {
17
+ log.info(`Hot reloading cds model...`)
18
+ const csn = await cds.compile('*');
19
+ const sqliteModel = cds.compile(csn).to.sql({ dialect: 'sqlite' })
20
+ const model = cds.compile.for.nodejs(csn)
21
+ if (cds.env.requires.db.impl === '@cap-js/sqlite') {
22
+ model.$schema = sqliteModel;
23
+ }
24
+ const hash = crypto.createHash('sha256')
25
+ await stream.pipeline(sqliteModel, hash)
26
+ model.$schemaHash = hash.digest('hex')
27
+ if (model.$schemaHash !== cds.model.$schemaHash) {
28
+ if (cds.env.requires.db.impl === '@cap-js/sqlite') {
29
+ const db = await cds.connect.to('db');
30
+ const { dropTables, dropViews, newTables, newViews } = sqlDiff(model, cds.model)
31
+ try {
32
+ await db.run([...dropViews, ...dropTables].map(item => `DROP ${item.type} IF EXISTS ${item.name}`))
33
+ cds.debug('cli')(`dropped ${[...dropViews, ...dropTables].length} views/tables during cds watch`)
34
+ } catch {
35
+ // Restart if error happens to ensure it still works
36
+ process.send({ code: 'restart' })
37
+ return;
38
+ }
39
+
40
+ try {
41
+ await db.run([...newTables, ...newViews].map(item => item.sql))
42
+ cds.debug('cli')(`created new views and tables on cds watch`)
43
+ } catch {
44
+ // Restart if error happens to ensure it still works
45
+ process.send({ code: 'restart' })
46
+ return;
47
+ }
48
+ // redeploy test data
49
+ await redeployData(csn, db, (file, e) => newTables.some(table => table.name.toUpperCase() === e.replaceAll('.', '_').toUpperCase()))
50
+ } else {
51
+ // Restart instance when DB changes cannot be solved by redeploying
52
+ process.send({ code: 'restart' })
53
+ return;
54
+ }
55
+ }
56
+ if (didMockDataChange(msg.eventsAndOpts ?? [])) {
57
+ const db = await cds.connect.to('db');
58
+ await redeployData(csn, db, (file) => msg.eventsAndOpts.some(({ name: fullPath }) => fullPath.endsWith(file)), true)
59
+ }
60
+ // Assign new model to CDS
61
+ cds.model = model
62
+
63
+ // Check for differences in services to restart if necessary and assign model to services as well
64
+ const services = Object.keys(model.definitions).filter(name => model.definitions[name].kind === 'service')
65
+ for (const srv in cds.services) {
66
+ const existingSrvIdx = services.findIndex(s => s === srv);
67
+
68
+ // Restart because an existing service was removed
69
+ // Avoid restarts for services without a model, for example pure rest remote services
70
+ if (existingSrvIdx < 0 && srv !== 'db' && cds.services[srv].model && cds.services[srv].definition) {
71
+ process.send({ code: 'restart' })
72
+ } else if (existingSrvIdx >= 0) {
73
+ const existingSrv = model.definitions[services.splice(existingSrvIdx, 1)];
74
+ // Ensure server restarts if protocol changes
75
+ if (
76
+ Object.keys(existingSrv.protocols).length !== Object.keys(cds.services[srv].definition.protocols).length ||
77
+ Object.keys(existingSrv.protocols).some(existingProtocol => !Object.keys(cds.services[srv].definition.protocols).some(protocol => protocol === existingProtocol))
78
+ ) {
79
+ process.send({ code: 'restart' })
80
+ return;
81
+ }
82
+
83
+ // Restart if @path annotation changes
84
+ if (existingSrv['@path'] !== cds.services[srv].definition['@path']) {
85
+ process.send({ code: 'restart' })
86
+ return;
87
+ }
88
+ }
89
+ // Services somehow have their own model cache, thus override is needed
90
+ cds.services[srv].model = model
91
+ }
92
+ // New service was added needing restart
93
+ if (services.length) {
94
+ process.send({ code: 'restart' })
95
+ return;
96
+ }
97
+ // This triggers the live-reload - its important to be at the end to avoid race-conditions
98
+ process.send({ code: 'model-refreshed' })
99
+ log.info(`...cds model reloaded.`)
100
+ }
101
+ })
102
+ }
103
+
104
+ /**
105
+ *
106
+ * @param {*} csn
107
+ * @param {*} db
108
+ * @param {*} includeFileFilter Function which is invoked to toggle which files are deployed
109
+ */
110
+ async function redeployData(csn, db, includeFileFilter, withUndeploy = false) {
111
+ const filesToDeploy = {}
112
+ const entitiesToUndeploy = []
113
+ const resources = await cds.deploy.resources(csn, { testdata: cds.env.features.test_data })
114
+ const resEntries = Object.entries(resources).reverse() // reversed $sources, relevant as UPSERT order
115
+ for (const [file, e] of resEntries) {
116
+ if (includeFileFilter(file, e)) {
117
+ let src = await cds.utils.read(file, 'utf8')
118
+ filesToDeploy[file] = src;
119
+ entitiesToUndeploy.push(e)
120
+ }
121
+ }
122
+ if (entitiesToUndeploy.length) {
123
+ try {
124
+ if (withUndeploy) {
125
+ await db.run(entitiesToUndeploy.map((entity) => DELETE.from(entity).where('1 = 1')))
126
+ }
127
+ await cds.deploy.data(db, csn, {}, filesToDeploy)
128
+ } catch {
129
+ // Full restart in case redeploy fails, that might happen with referential constraints
130
+ process.send({ code: 'restart' })
131
+ return;
132
+ }
133
+ }
134
+ }
135
+
136
+ function didMockDataChange(eventsAndOpts) {
137
+ for (const { name: path } of eventsAndOpts) {
138
+ if ((new RegExp(`test.*data.*\\.(csv|json)`)).test(path) || cds.env.roots.some(root => (new RegExp(`${root}.*data.*\\.(csv|json)`)).test(path))) {
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+
145
+ const createRegex = /CREATE\s+(TABLE|VIEW)\s+([^\s(]+)/gi
146
+ const parseSchema = (schema) => {
147
+ const statements = {}
148
+ const sql = schema.join('\n')
149
+ let match
150
+ while ((match = createRegex.exec(sql)) !== null) {
151
+ const type = match[1].toUpperCase()
152
+ const name = match[2]
153
+ statements[name] = { type, name }
154
+ }
155
+ return statements
156
+ }
157
+ function sqlDiff(model, cdsModel) {
158
+ const oldSchema = parseSchema(cdsModel.$schema || [])
159
+ const newSchema = parseSchema(model.$schema || [])
160
+
161
+ // Find changed or removed tables/views
162
+ const toRecreate = []
163
+ for (const name in oldSchema) {
164
+ if (!newSchema[name]) {
165
+ // Removed - need to drop
166
+ toRecreate.push({ ...oldSchema[name], action: 'drop' })
167
+ } else {
168
+ // Check if definition changed by comparing the actual SQL
169
+ const oldSql = (cdsModel.$schema || []).find(s => s.includes(`CREATE ${oldSchema[name].type} ${name}`))
170
+ const newSql = (model.$schema || []).find(s => s.includes(`CREATE ${newSchema[name].type} ${name}`))
171
+ if (oldSql !== newSql) {
172
+ toRecreate.push({ ...oldSchema[name], action: 'drop' })
173
+ toRecreate.push({ ...newSchema[name], action: 'create', sql: newSql })
174
+ }
175
+ }
176
+ }
177
+
178
+ // Find new tables/views
179
+ for (const name in newSchema) {
180
+ if (!oldSchema[name]) {
181
+ const newSql = (model.$schema || []).find(s => s.includes(`CREATE ${newSchema[name].type} ${name}`))
182
+ toRecreate.push({ ...newSchema[name], action: 'create', sql: newSql })
183
+ }
184
+ }
185
+
186
+ // Execute drops first (views before tables)
187
+ const drops = toRecreate.filter(t => t.action === 'drop')
188
+ const views = drops.filter(t => t.type === 'VIEW').reverse()
189
+ const tables = drops.filter(t => t.type === 'TABLE').reverse()
190
+
191
+ // Execute creates (tables before views)
192
+ const creates = toRecreate.filter(t => t.action === 'create')
193
+ const newTables = creates.filter(t => t.type === 'TABLE')
194
+ const newViews = creates.filter(t => t.type === 'VIEW')
195
+
196
+ return {
197
+ dropViews: views,
198
+ dropTables: tables,
199
+ newTables,
200
+ newViews
201
+ }
202
+ }
@@ -1,6 +1,5 @@
1
1
  const DEBUG = /\b(y|all|cli|watch|livereload)\b/.test (process.env.DEBUG) ? ((..._) => console.debug ('[cli] -', ..._)) : undefined
2
2
 
3
- const axios = require('axios')
4
3
  const ws = require('ws')
5
4
  const express = require('express')
6
5
  const { URL } = require('url')
@@ -9,7 +8,7 @@ const escapedSep = sep === '\\' ? '\\\\' : sep
9
8
 
10
9
  const ext = 'css,gif,html,jpg,png,svg,ts,mjs,cjs,js,json,properties,change,variant,ctrl_variant,ctrl_variant_change,ctrl_variant_management_change'
11
10
  const watchIncludes = RegExp(`\\.(${ext.replace(/,/g,'|')})$|\\${sep}(webapp|appconfig)`)
12
- const watchExcludes = new RegExp(`${escapedSep}(node_modules|\\.git|dist)(${escapedSep}|$)|@cds-models${escapedSep}|@types${escapedSep}`)
11
+ const watchExcludes = new RegExp(`${escapedSep}(node_modules|\\.git|dist)(${escapedSep}|$)|@cds-models${escapedSep}|@types${escapedSep}|${escapedSep}test${escapedSep}|${escapedSep}workflow${escapedSep}|package-lock\\.json|\\.appGenInfo\\.json|-generator-config\\.json|xs-app\\.json|\\.vscode`)
13
12
 
14
13
  module.exports = class LiveReload {
15
14
  async start(port, cwd, delay=0) {
@@ -86,6 +85,9 @@ module.exports = class LiveReload {
86
85
  // Watching for touched files...
87
86
  const watch = require('./watcher')
88
87
  const filter = f => {
88
+ // Approximately ensure that directories do not cause live reloads
89
+ const lastSegment = f.split(sep).pop()
90
+ if (!lastSegment.includes('.')) return false
89
91
  const isIn = !watchExcludes.test(f) && watchIncludes.test(f)
90
92
  if (!isIn) DEBUG?. (`livereload ignored: ${relative(cwd||process.cwd(), f)}`)
91
93
  // else DEBUG?. (`livereload NOT ignored: ${relative(cwd||process.cwd(), f)}`, watchIncludes)
@@ -146,8 +148,8 @@ async function livereloadURL(address, port) {
146
148
  if (process.env.WS_BASE_URL) {
147
149
  // on BAS, expose the livereload port as an externally accessible URL
148
150
  try {
149
- const resp = await axios.get('http://localhost:3001/AppStudio/api/getHostByPort?port=' + port)
150
- return resp.data.result + 'livereload.js?snipver=1&port=443'
151
+ const resp = await fetch('http://localhost:3001/AppStudio/api/getHostByPort?port=' + port, { method: 'GET' })
152
+ return (await resp.json()).result + 'livereload.js?snipver=1&port=443'
151
153
  } catch (e) {
152
154
  console.error(`Error exposing live reload port ${port}: ${e.message}`)
153
155
  return
@@ -31,6 +31,7 @@ module.exports = ({ cwd = process.cwd(), script, ext, ignore=RegExp(), watched,
31
31
 
32
32
  const updated = eventsAndOpts.filter(evt => evt.type && evt.type === 'update').map(evt => evt.name)
33
33
  emitter.emit ('restart', updated)
34
+ env.CDS_WATCH_HOT_RELOAD = options['experimental-hot-reload']
34
35
  child = fork (script, { cwd, env, stdio:'inherit', execArgv }, (err)=>{
35
36
  if (err) console.error (err)
36
37
  })
@@ -57,9 +58,12 @@ module.exports = ({ cwd = process.cwd(), script, ext, ignore=RegExp(), watched,
57
58
  // else DEBUG?. (`NOT ignored: ${relative(cwd||process.cwd(), f)}`, include.test(f), include)
58
59
  return isIn
59
60
  }
60
- const watcher = watch (watched,{ filter, ignore })
61
+ const watcher = watch (watched,{ filter, ignore, hotReload: options['experimental-hot-reload'] })
61
62
  if (DEBUG) watcher.on('close', ()=> DEBUG('⚡️', 'cds watch - file watcher closed'))
62
63
  watcher.on('change', restart)
64
+ watcher.on('refreshModel', _coalesceEvents(delay, (eventsAndOpts) => {
65
+ child.send({ code:'refreshModel', eventsAndOpts })
66
+ }))
63
67
  process.on('shutdown', ()=> watcher.close())
64
68
 
65
69
  // Live commands...
@@ -3,15 +3,19 @@
3
3
  // Extracted from watch.js to avoid cds.env loaded too eagerly
4
4
  //
5
5
 
6
- (async()=>{
6
+ (async () => {
7
7
 
8
8
  // run through cli in order to use its error handling
9
9
  const args = JSON.parse(process.env._args)
10
10
  const cds = require('../cds'); cds.watched = true
11
11
  const bootstrap = require('../../bin/cds')(...args)
12
12
 
13
- cds.once('listening', ({server}) => {
14
- process.send ({ code:'listening', port: server.address().port })
13
+ cds.once('listening', async ({ server }) => {
14
+ process.send({ code: 'listening', port: server.address().port })
15
+ if (process.env.CDS_WATCH_HOT_RELOAD && process.env.CDS_WATCH_HOT_RELOAD !== 'false') {
16
+ const hotReload = require('./hot-reload');
17
+ await hotReload()
18
+ }
15
19
  })
16
20
 
17
21
  try {
@@ -2,13 +2,47 @@ const fs = require('node:fs')
2
2
  const { resolve, sep } = require('node:path')
3
3
  const { EventEmitter } = require('node:events')
4
4
 
5
- module.exports = function watch(paths, { filter, ignore } = {}) {
5
+ module.exports = function watch(paths, { filter, ignore, hotReload } = {}) {
6
6
  const ee = new EventEmitter()
7
7
  const roots = [...new Set((Array.isArray(paths) ? paths : [paths]).filter(Boolean).map(p => resolve(p)))]
8
8
  const ac = new AbortController() // eslint-disable-line no-undef
9
9
  let closed = false
10
10
 
11
11
  const _allow = p => !filter || (filter instanceof RegExp ? filter.test(p) : filter(p))
12
+ const dataFiles = hotReload && [
13
+ new RegExp(`test.*data.*\\.(csv|json)`),
14
+ new RegExp(`db.*data.*\\.(csv|json)`)
15
+ ]
16
+ const _hotReload = !hotReload ? ()=>false : fullPath => {
17
+ if (!hotReload || !_allow(fullPath)) return false;
18
+ // Hot reload all CDS files, services are
19
+ if (fullPath.endsWith('.cds')) return true;
20
+ // .properties files are used for i18n -> hot reload i18n
21
+ if (fullPath.endsWith('.properties') && fullPath.includes('i18n')) return true;
22
+ // .json & .csv files used in data folders trigger a hot reload, e.g. delete and insert for the table
23
+ if (dataFiles.some(regex => regex.test(fullPath))) {
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ const reloadRelevantJSONfiles = hotReload ? [
30
+ new RegExp(`\\.cdsrc\\.json`),
31
+ new RegExp(`\\package\\.json`),
32
+ new RegExp(`\\.cdsrc-private\\.json`),
33
+ new RegExp(`default-env\\.json`),
34
+ ] : []
35
+ /**
36
+ * Helper function to in case of hot reload restrict reloads based on .json files to just
37
+ * .cdsrc.json, cdsrc-private.json and root package.json
38
+ */
39
+ const _narrowJSONreloads = !hotReload ? ()=> true : fullPath => {
40
+ if (!hotReload) return true;
41
+ if (reloadRelevantJSONfiles.some(regex => regex.test(fullPath))) {
42
+ return true;
43
+ }
44
+ return false;
45
+ }
12
46
 
13
47
  const useNativeRecursive = process.platform === 'darwin' || process.platform === 'win32'
14
48
  if (useNativeRecursive) {
@@ -20,7 +54,9 @@ module.exports = function watch(paths, { filter, ignore } = {}) {
20
54
  if (closed) break
21
55
  const name = event.filename?.toString?.() ?? event.filename ?? ''
22
56
  const fullPath = resolve(root, name)
23
- if (_allow(fullPath)) {
57
+ if (_hotReload(fullPath)) {
58
+ ee.emit('refreshModel', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath)
59
+ } else if (_allow(fullPath) && _narrowJSONreloads(fullPath)) {
24
60
  ee.emit('change', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath)
25
61
  }
26
62
  }
@@ -84,7 +120,9 @@ module.exports = function watch(paths, { filter, ignore } = {}) {
84
120
  if (fs.lstatSync(fullPath).isDirectory()) _enqueue(fullPath)
85
121
  } catch { /* ignore */ }
86
122
  }
87
- if (_allow(fullPath)) {
123
+ if (_hotReload(fullPath)) {
124
+ ee.emit('refreshModel', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath)
125
+ } else if (_allow(fullPath) && _narrowJSONreloads(fullPath)) {
88
126
  ee.emit('change', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath)
89
127
  }
90
128
  }