@sap/cds-dk 9.7.2 → 9.8.1
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 +42 -2
- package/bin/bind.js +7 -2
- package/bin/build.js +14 -4
- package/bin/cds.js +2 -2
- package/bin/completion/completionFs.js +1 -1
- package/bin/completion/index.js +2 -2
- package/bin/data-products/get-csn-via-dp-cli.js +1 -1
- package/bin/debug.js +1 -1
- package/bin/deploy/index.js +3 -2
- package/bin/gen.js +1 -1
- package/bin/help.js +1 -1
- package/bin/import.js +3 -0
- package/bin/minify.js +1 -1
- package/bin/mock.js +1 -1
- package/bin/repl.js +212 -39
- package/bin/test.js +0 -2
- package/bin/up.js +27 -3
- package/bin/util/containerize.js +1 -3
- package/bin/version.js +30 -9
- package/bin/watch.js +20 -4
- package/lib/bind/cf.js +2 -2
- package/lib/bind/k8s.js +0 -1
- package/lib/bind/plugin.js +2 -0
- package/lib/bind/shared.js +6 -1
- package/lib/build/constants.js +0 -8
- package/lib/build/index.js +569 -25
- package/lib/build/{provider/edmxBuildPlugin.js → plugins/edmx.js} +6 -15
- package/lib/build/{provider/featureToggleBuildPlugin.js → plugins/featureToggles.js} +8 -27
- package/lib/build/{provider → plugins}/fiori/index.js +27 -40
- package/lib/build/{provider → plugins}/hana/index.js +67 -85
- package/lib/build/{provider → plugins}/hana/migrationtable.js +4 -3
- package/lib/build/plugins/helm/index.js +6 -6
- package/lib/build/{provider/internalBuildPlugin.js → plugins/internal.js} +7 -31
- package/lib/build/{provider → plugins}/java/index.js +18 -26
- package/lib/build/plugins/mtx/index.js +41 -0
- package/lib/build/plugins/mtx/resourcesTarBuilder.js +41 -0
- package/lib/build/{provider → plugins}/mtx-extension/extensionCompilation.js +1 -1
- package/lib/build/{provider → plugins}/mtx-extension/index.js +35 -44
- package/lib/build/{provider → plugins}/mtx-sidecar/index.js +17 -3
- package/lib/build/{provider → plugins}/nodejs/index.js +29 -28
- package/lib/build/{plugin.js → plugins/plugin.js} +57 -60
- package/lib/build/plugins.js +11 -8
- package/lib/build/util.js +0 -8
- package/lib/compile/to-xsuaa.js +2 -2
- package/lib/deploy/to-hana/hana.js +64 -56
- package/lib/import/common.js +1 -1
- package/lib/import/odata/metadataconverter/MetadataConverter.js +4 -3
- package/lib/import/odata/v2parser.js +28 -28
- package/lib/import/odata/v4parser.js +37 -2
- package/lib/import/openapi/index.js +1 -1
- package/lib/init/constants.js +1 -2
- package/lib/init/template/app-frontend/index.js +1 -1
- package/lib/init/template/approuter/files/package.json +1 -1
- package/lib/init/template/completion/completionSetup.js +1 -1
- package/lib/init/template/data/as-csv.js +1 -1
- package/lib/init/template/github-actions/index.js +1 -1
- package/lib/init/template/handler/index.js +1 -1
- package/lib/init/template/helm/index.js +4 -4
- package/lib/init/template/kyma/index.js +3 -3
- package/lib/init/template/lint/index.js +7 -21
- package/lib/init/template/multitenancy/files/package.json.hbs +0 -5
- package/lib/init/template/postgres/files/containerize.yaml.hbs +15 -0
- package/lib/init/template/postgres/files/package.json.hbs +35 -0
- package/lib/init/template/postgres/files/values.yaml.hbs +23 -0
- package/lib/init/template/postgres/index.js +34 -9
- package/lib/init/template/test/files/package.json +1 -1
- package/lib/init/template/typescript/files/package.json +1 -1
- package/lib/init/template/workzone-standard/files/mta.yaml.hbs +1 -0
- package/lib/lint/eslintHelp.json +1 -1
- package/lib/lint/io.js +9 -13
- package/lib/mtx/pull.js +1 -1
- package/lib/mtx/settings_manager.js +11 -5
- package/lib/mtx/tenant_action.js +1 -1
- package/lib/mtx/util/fs.js +2 -2
- package/lib/util/fs.js +3 -3
- package/lib/util/git.js +1 -1
- package/lib/util/glob.js +1 -1
- package/lib/watch/hot-reload.js +202 -0
- package/lib/watch/livereload-server.js +6 -4
- package/lib/watch/node-watch.js +5 -1
- package/lib/watch/watched.js +7 -3
- package/lib/watch/watcher.js +41 -3
- package/npm-shrinkwrap.json +46 -31
- package/package.json +1 -1
- package/lib/build/buildTaskEngine.js +0 -377
- package/lib/build/buildTaskFactory.js +0 -198
- package/lib/build/buildTaskProvider.js +0 -38
- package/lib/build/buildTaskProviderFactory.js +0 -233
- package/lib/build/provider/internalBuildTaskProvider.js +0 -227
- package/lib/build/provider/java-cf/index.js +0 -1
- package/lib/build/provider/mtx/index.js +0 -36
- package/lib/build/provider/mtx/resourcesTarBuilder.js +0 -102
- package/lib/build/provider/node-cf/index.js +0 -1
- package/lib/init/template/lint/files/.eslint/rules/no-entity-moo.js +0 -40
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/fixed/schema.cds +0 -4
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/invalid/schema.cds +0 -4
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/rule.test.js +0 -15
- package/lib/init/template/lint/files/.eslint/tests/no-entity-moo/valid/schema.cds +0 -6
- package/lib/init/types.d.ts +0 -23
- /package/lib/build/{provider → plugins}/hana/2migration.js +0 -0
- /package/lib/build/{provider → plugins}/hana/template/.hdiconfig-all +0 -0
- /package/lib/build/{provider → plugins}/hana/template/.hdiconfig-cloud +0 -0
- /package/lib/build/{provider → plugins}/hana/template/.hdinamespace +0 -0
- /package/lib/build/{provider → plugins}/hana/template/package.json +0 -0
- /package/lib/build/{provider → plugins}/hana/template/undeploy.json +0 -0
package/lib/lint/eslintHelp.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "9.39.
|
|
2
|
+
"version": "9.39.4",
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/lib/mtx/tenant_action.js
CHANGED
|
@@ -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
|
|
84
|
+
} catch {
|
|
85
85
|
try {
|
|
86
86
|
libPath = require.resolve(libId);
|
|
87
87
|
DEBUG?.(`Using cds-mtxs from ${libPath}`);
|
package/lib/mtx/util/fs.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
86
|
-
const queue = /** @type {
|
|
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
package/lib/util/glob.js
CHANGED
|
@@ -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
|
|
150
|
-
return resp.
|
|
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
|
package/lib/watch/node-watch.js
CHANGED
|
@@ -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...
|
package/lib/watch/watched.js
CHANGED
|
@@ -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
|
|
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 {
|
package/lib/watch/watcher.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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
|
}
|