@sap/cds-dk 9.8.4 → 9.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/bin/add.js +3 -3
  3. package/bin/cds.js +16 -2
  4. package/bin/import.js +6 -2
  5. package/bin/logout.js +9 -12
  6. package/lib/bind/shared.js +2 -2
  7. package/lib/build/constants.js +0 -1
  8. package/lib/build/index.js +420 -463
  9. package/lib/build/plugins/hana/index.js +121 -2
  10. package/lib/build/plugins/helm/files/subcharts/content-deployment/Chart.yaml +1 -1
  11. package/lib/build/plugins/helm/files/subcharts/content-deployment/templates/job.yaml +1 -1
  12. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.schema.json +4 -0
  13. package/lib/build/plugins/helm/files/subcharts/content-deployment/values.yaml +5 -2
  14. package/lib/build/plugins/internal.js +1 -1
  15. package/lib/build/plugins/mtx-extension/index.js +0 -2
  16. package/lib/deploy/index.js +23 -3
  17. package/lib/deploy/to-hana/hana.js +24 -15
  18. package/lib/import/common.js +6 -3
  19. package/lib/init/constants.js +3 -1
  20. package/lib/init/indent.js +0 -1
  21. package/lib/init/merge.js +0 -1
  22. package/lib/init/mvn.js +4 -2
  23. package/lib/init/projectReader.js +0 -1
  24. package/lib/init/registries/mta.js +4 -2
  25. package/lib/init/template/app-frontend/index.js +39 -4
  26. package/lib/init/template/data/as-csv.js +2 -0
  27. package/lib/init/template/esm/index.js +5 -0
  28. package/lib/init/template/extensibility/index.js +1 -1
  29. package/lib/init/template/hana/index.js +1 -1
  30. package/lib/init/template/html5-repo/files/mta.yaml.hbs +9 -20
  31. package/lib/init/template/http/index.js +34 -6
  32. package/lib/init/template/kyma/index.js +1 -0
  33. package/lib/init/template/postgres/files/values.yaml.hbs +1 -1
  34. package/lib/init/template/postgres/index.js +1 -1
  35. package/lib/init/template/react/files/index.html +13 -0
  36. package/lib/init/template/react/files/package.json.hbs +22 -0
  37. package/lib/init/template/react/files/public/favicon.svg +1 -0
  38. package/lib/init/template/react/files/src/App.jsx.hbs +29 -0
  39. package/lib/init/template/react/files/src/index.css +28 -0
  40. package/lib/init/template/react/files/src/main.jsx +10 -0
  41. package/lib/init/template/react/files/vite.config.js +19 -0
  42. package/lib/init/template/react/index.js +29 -0
  43. package/lib/init/template/sample/index.js +1 -4
  44. package/lib/init/template/typer/index.js +1 -1
  45. package/lib/init/template/typescript/files/package.json +1 -1
  46. package/lib/init/template/typescript/index.js +1 -1
  47. package/lib/init/template/ui5/index.js +2 -1
  48. package/lib/init/template/vue/files/index.html +13 -0
  49. package/lib/init/template/vue/files/package.json.hbs +19 -0
  50. package/lib/init/template/vue/files/public/favicon.svg +1 -0
  51. package/lib/init/template/vue/files/src/App.vue.hbs +34 -0
  52. package/lib/init/template/vue/files/src/main.js +5 -0
  53. package/lib/init/template/vue/files/src/style.css +20 -0
  54. package/lib/init/template/vue/files/vite.config.js +19 -0
  55. package/lib/init/template/vue/index.js +29 -0
  56. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +2 -13
  57. package/lib/init/xml.js +0 -2
  58. package/lib/mtx/auth_manager.js +21 -11
  59. package/lib/mtx/download_legacy.js +9 -8
  60. package/lib/mtx/pull.js +9 -8
  61. package/lib/mtx/push.js +42 -28
  62. package/lib/mtx/settings_manager.js +17 -10
  63. package/lib/mtx/tenant_action.js +9 -9
  64. package/lib/mtx/util/request.js +158 -0
  65. package/lib/util/cf.js +30 -29
  66. package/lib/watch/node-watch.js +1 -1
  67. package/lib/watch/vite-watch.js +76 -0
  68. package/lib/watch/watched.js +5 -4
  69. package/npm-shrinkwrap.json +14 -154
  70. package/package.json +1 -2
  71. package/lib/init/template/sample/files/db/hana/index.cds +0 -10
  72. package/lib/init/template/sample/files/db/sqlite/index.cds +0 -10
  73. package/lib/init/template/sample/files/package.json.hbs +0 -14
  74. package/lib/util/axios.js +0 -118
package/lib/mtx/push.js CHANGED
@@ -1,15 +1,13 @@
1
1
  const { utils: { local }} = require ('../cds');
2
2
  const { join } = require('path');
3
3
  const { readFileSync, existsSync } = require('fs');
4
- const axios = require('axios');
5
4
 
6
5
  const { login } = require('./auth_manager');
7
6
  const buildSystem = require('../build')
8
7
 
9
- require('../util/axios').pruneErrors();
8
+ const { buildResponseError, buildNetworkError, mtxFetch } = require('./util/request');
10
9
  const { getMessage } = require('./util/logging');
11
10
  const { schemaRegex } = require('./util/urls');
12
- const { handleHttpError } = require('./util/errors');
13
11
 
14
12
  const JOB_STATUS = {
15
13
  QUEUED: 'QUEUED',
@@ -35,9 +33,9 @@ module.exports = class Push {
35
33
  const target = { url, ...subdomain && { subdomain } };
36
34
 
37
35
  console.log(`\nPushing extension '${extensionName}' from`, { src }, 'to', target);
38
- const push = await this.pushTgz(params, content, extensionName);
36
+ const pushResult = await this.pushTgz(params, content, extensionName);
39
37
  if (params.get('async')) {
40
- await this.pollUntilFinished(push.headers.location, params)
38
+ await this.pollUntilFinished(pushResult.headers.get('location'), params)
41
39
  }
42
40
  console.log ('Activation succeeded.\n')
43
41
  }
@@ -52,11 +50,8 @@ module.exports = class Push {
52
50
  if (!schemaRegex.test(src)) {
53
51
  throw getMessage(`Non-existent path: ${src}`, { command: 'push' });
54
52
  }
55
- try {
56
- content = (await axios.get(src, { responseEncoding: 'base64' })).data;
57
- } catch (error) {
58
- handleHttpError(error, params, { url: src });
59
- }
53
+ const response = await mtxFetch('GET', src, {}, params);
54
+ content = Buffer.from(await response.arrayBuffer()).toString('base64');
60
55
  }
61
56
  return { src, content };
62
57
  }
@@ -71,40 +66,59 @@ module.exports = class Push {
71
66
 
72
67
  static async pushTgz(params, tgz, extensionName) {
73
68
  const pushUrl = `${params.get('appUrl')}/-/cds/extensibility/push`;
74
- const options = { ...params.get('reqAuth') };
69
+ const reqAuth = params.get('reqAuth') || {};
70
+ const headers = { 'content-type': 'application/json', ...reqAuth.headers };
75
71
  const async = params.get('async');
76
72
  if (async) {
77
- options.headers = { prefer: 'respond-async', ...options.headers }
73
+ headers['prefer'] = 'respond-async';
78
74
  }
79
75
 
80
- return axios.post(pushUrl, {
81
- extension: tgz,
82
- tag: extensionName
83
- }, options)
84
- .catch(error => handleHttpError(error, params, { url: pushUrl }));
76
+ const response = await mtxFetch('POST', pushUrl, {
77
+ headers,
78
+ body: JSON.stringify({ extension: tgz, tag: extensionName })
79
+ }, params);
80
+ return response;
85
81
  }
86
82
 
87
83
  static async pollUntilFinished(jobUrl, params) {
88
- return new Promise((resolve, reject) => {
89
- const options = { ...params.get('reqAuth') };
84
+ const reqAuth = params.get('reqAuth') || {};
85
+ const headers = { ...reqAuth.headers };
90
86
 
87
+ return new Promise((resolve, reject) => {
91
88
  const timeout = setTimeout(() => {
92
89
  clearInterval(interval);
93
90
  reject(new Error('cds push timed out after 5 minutes'));
94
91
  }, 5 * 60 * 1000);
95
92
 
96
93
  const interval = setInterval(async () => {
97
- const jobResult = await axios.get(jobUrl, options);
98
- const { status, tasks } = jobResult.data;
99
- const error = tasks[0]?.error ?? 'cds push failed'
100
- if (status === JOB_STATUS.FINISHED || status === JOB_STATUS.FAILED) {
94
+ try {
95
+ const response = await fetch(jobUrl, { headers });
96
+ if (!response.ok) {
97
+ clearInterval(interval);
98
+ clearTimeout(timeout);
99
+ reject(await buildResponseError('GET', jobUrl, response));
100
+ return;
101
+ }
102
+ const jobResult = await response.json();
103
+ const { status, tasks } = jobResult;
104
+ const error = tasks[0]?.error ?? 'cds push failed'
105
+ if (status === JOB_STATUS.FINISHED || status === JOB_STATUS.FAILED) {
106
+ clearInterval(interval);
107
+ clearTimeout(timeout);
108
+
109
+ if (status === JOB_STATUS.FINISHED) {
110
+ resolve();
111
+ } else if (status === JOB_STATUS.FAILED) {
112
+ reject(new Error(error));
113
+ }
114
+ }
115
+ } catch (e) {
101
116
  clearInterval(interval);
102
117
  clearTimeout(timeout);
103
-
104
- if (status === JOB_STATUS.FINISHED) {
105
- resolve();
106
- } else if (status === JOB_STATUS.FAILED) {
107
- reject(new Error(error));
118
+ if (e.status) {
119
+ reject(e);
120
+ } else {
121
+ reject(buildNetworkError('GET', jobUrl, e));
108
122
  }
109
123
  }
110
124
  }, 1000);
@@ -1,14 +1,13 @@
1
1
  const fs = require('fs');
2
2
  const os = require('os');
3
3
  const path = require('path');
4
- const axios = require('axios');
5
-
6
4
  const cds = require('../cds');
7
5
 
8
6
  const { requireGlobal } = require('./util/dependencies');
9
7
  const { getAppFromSuggestions, getSubdomain } = require('./util/cf');
10
8
  const isBas = require('./util/env');
11
9
  const { getMessage } = require('./util/logging');
10
+ const { buildResponseError, buildNetworkError, parseResponseBody } = require('./util/request');
12
11
  const Question = require('./util/question');
13
12
  const { httpSchema, httpsSchema, schemaRegex, localhostRegex } = require('./util/urls');
14
13
  const { capitalize } = require('./util/strings');
@@ -111,20 +110,28 @@ async function getTokenUrl(params, renewUrl) {
111
110
  }
112
111
 
113
112
  async function fetchPasscodeUrl(params) {
114
- const url = appUrlWith(OAUTH_META_PATH, params);
115
- DEBUG?.(`Trying to get passcode URL from GET`, url);
113
+ let fetchUrl = appUrlWith(OAUTH_META_PATH, params);
114
+ DEBUG?.(`Trying to get passcode URL from GET`, fetchUrl);
115
+ if (params.has('subdomain')) {
116
+ const u = new URL(fetchUrl);
117
+ u.searchParams.set('subdomain', params.get('subdomain'));
118
+ fetchUrl = u.toString();
119
+ }
116
120
  let responseData;
117
121
  try {
118
- const config = params.has('subdomain')
119
- ? { params: { subdomain: params.get('subdomain') } }
120
- : undefined;
121
- const response = await axios.get(url, config /* no auth required */);
122
- responseData = response.data;
122
+ const response = await fetch(fetchUrl /* no auth required */);
123
+ if (!response.ok) {
124
+ throw await buildResponseError('GET', fetchUrl, response);
125
+ }
126
+ responseData = await parseResponseBody(response);
123
127
  } catch (error) {
124
128
  if (error.status === 404) {
125
129
  DEBUG?.(`Request unsupported by server, ignoring (${error.message.replace(/ *Details:.*/s, '')})`);
126
- } else {
130
+ } else if (error.status) {
127
131
  DEBUG?.(getMessage(`Getting passcode URL failed`, { error }));
132
+ } else {
133
+ const netError = buildNetworkError('GET', fetchUrl, error);
134
+ DEBUG?.(getMessage(`Getting passcode URL failed`, { error: netError }));
128
135
  }
129
136
  return undefined;
130
137
  }
@@ -3,10 +3,9 @@ const { ParamCollection } = require('./params');
3
3
  const AuthManager = require('./auth_manager');
4
4
  const { localhostRegex } = require('./util/urls');
5
5
 
6
- require('../util/axios').pruneErrors();
6
+ const { mtxFetch } = require('./util/request');
7
7
  const { getMessage } = require('./util/logging');
8
8
  const { capitalize } = require('./util/strings');
9
- const { handleHttpError } = require('./util/errors');
10
9
 
11
10
  const DEBUG = cds.debug('cli');
12
11
 
@@ -63,16 +62,17 @@ module.exports = class TenantAction {
63
62
  let parsedBody;
64
63
  if (body) {
65
64
  try {
66
- parsedBody = JSON.parse(body)
65
+ parsedBody = JSON.parse(body);
67
66
  } catch (error) {
68
67
  throw getMessage('Invalid subscription body', { error, command: this.command });
69
68
  }
70
69
  }
71
- try {
72
- await require('axios').post(url, { tenant, metadata: parsedBody, options: parsedBody }, params.get('reqAuth'));
73
- } catch (error) {
74
- handleHttpError(error, params, { url });
75
- }
70
+ const reqAuth = params.get('reqAuth') || {};
71
+ const headers = { 'content-type': 'application/json', ...reqAuth.headers };
72
+ await mtxFetch('POST', url, {
73
+ headers,
74
+ body: JSON.stringify({ tenant, metadata: parsedBody, options: parsedBody })
75
+ }, params);
76
76
  }
77
77
 
78
78
  async runWithServer(params, tenant) {
@@ -91,7 +91,7 @@ module.exports = class TenantAction {
91
91
  }
92
92
 
93
93
  const { cds_mtx } = require(libPath);
94
- const body = params.get('body')
94
+ const body = params.get('body');
95
95
  return cds_mtx(this.command, tenant, body);
96
96
  }
97
97
  }
@@ -0,0 +1,158 @@
1
+ const { inspect } = require('util');
2
+
3
+ const debug = require('../../cds').log('req');
4
+
5
+ function stacklessError(message) {
6
+ const { stackTraceLimit } = Error;
7
+ Error.stackTraceLimit = 0;
8
+ const error = new Error(message);
9
+ Error.stackTraceLimit = stackTraceLimit;
10
+ return error;
11
+ }
12
+
13
+ function extractDetails(data) {
14
+ if (!data) return undefined;
15
+ if (typeof data === 'string') {
16
+ if (/<html/i.test(data)) {
17
+ return '(HTML response received. If applicable, ensure the route to MTX is configured correctly in App Router.)';
18
+ }
19
+ if (data.length >= 1000) return undefined;
20
+ return data.trim();
21
+ }
22
+ if (typeof data === 'object') {
23
+ if (data.error_description) return data.error_description;
24
+ if (data.error && typeof data.error === 'object' && data.error.message) return data.error.message;
25
+ if (data.error && typeof data.error === 'string') return data.error;
26
+ if (typeof data.message === 'string') return data.message;
27
+ return inspect(data);
28
+ }
29
+ return undefined;
30
+ }
31
+
32
+ function buildError(method, url, { status, statusText, data, errno, code, fetchError } = {}) {
33
+ const obfuscateUrl = url => url?.replace(/(passcode|refreshToken|clientsecret|key)=[^&]+/g, '$1=...');
34
+ const prefix = (url && method ? `${method.toUpperCase()} ${obfuscateUrl(url)}` : 'Request') + ' failed';
35
+ let error;
36
+
37
+ if (errno || code) {
38
+ const errInfo = [errno, code].filter(Boolean).join(' ');
39
+ const message = prefix +
40
+ `: ${errInfo}` +
41
+ (/\b(ENOTFOUND|EAI_AGAIN)\b/.test(code)
42
+ ? `. Make sure the URL is correct and the server is running.`
43
+ : '');
44
+ error = stacklessError(message);
45
+ } else {
46
+ const details = extractDetails(data);
47
+ const reason = typeof data?.error === 'string' ? data.error : undefined;
48
+ const errObj = debug._debug && typeof data?.error === 'object'
49
+ ? inspect(data.error)
50
+ : undefined;
51
+
52
+ const message = prefix +
53
+ (status || statusText || reason || details
54
+ ? (status || statusText ? ':' : '') +
55
+ (status ? ` ${status}` : '') +
56
+ (statusText ? ` ${statusText}` : '') +
57
+ (reason ? `. ${reason}` : '') +
58
+ (details ? `. Details: ${details}` : '') +
59
+ (errObj ? `. Error object: ${errObj}` : '')
60
+ : '');
61
+ error = stacklessError(message);
62
+ if (status) error.status = status;
63
+ if (data?.passcode_url) {
64
+ error.auth = { passcode_url: data.passcode_url };
65
+ }
66
+ }
67
+
68
+ if (debug._debug && fetchError) {
69
+ error.cause = fetchError;
70
+ }
71
+
72
+ return error;
73
+ }
74
+
75
+ async function parseResponseBody(response) {
76
+ const contentType = response.headers.get('content-type') || '';
77
+ if (contentType.includes('application/json')) {
78
+ return response.json();
79
+ }
80
+ const text = await response.text();
81
+ try {
82
+ return JSON.parse(text);
83
+ } catch { /* ignore */ }
84
+ return text;
85
+ }
86
+
87
+ /**
88
+ * Build a fetch error from a non-ok response.
89
+ * Reads the response body and constructs a rich error with .status, .message, and .auth.
90
+ *
91
+ * @param {string} method - HTTP method
92
+ * @param {string} url - Request URL
93
+ * @param {Response} response - Fetch Response object
94
+ * @returns {Promise<Error>} error with .status, .message, .auth properties
95
+ */
96
+ async function buildResponseError(method, url, response) {
97
+ let data;
98
+ try {
99
+ data = await parseResponseBody(response);
100
+ } catch { /* ignore */ }
101
+ return buildError(method, url, {
102
+ status: response.status,
103
+ statusText: response.statusText,
104
+ data,
105
+ fetchError: new Error(`${response.status} ${response.statusText}`)
106
+ });
107
+ }
108
+
109
+ /**
110
+ * Build a fetch error from a network/system error.
111
+ *
112
+ * @param {string} method - HTTP method
113
+ * @param {string} url - Request URL
114
+ * @param {Error} fetchError - The caught error from fetch()
115
+ * @returns {Error} error with formatted message
116
+ */
117
+ function buildNetworkError(method, url, fetchError) {
118
+ const cause = fetchError.cause ?? {};
119
+ const errno = fetchError.errno || cause.errno;
120
+ const code = fetchError.code || cause.code
121
+ || fetchError.message?.match(/\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH)\b/)?.[1]
122
+ || cause.message?.match(/\b(ENOTFOUND|EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH)\b/)?.[1];
123
+ return buildError(method, url, { errno, code, fetchError });
124
+ }
125
+
126
+ const { handleHttpError } = require('./errors');
127
+
128
+ /**
129
+ * Fetch with standardized error handling for mtx modules.
130
+ * Network errors are wrapped via buildNetworkError.
131
+ * HTTP errors are passed through handleHttpError which maps status codes to user-facing messages.
132
+ *
133
+ * @param {string} method - HTTP method
134
+ * @param {string} url - Request URL
135
+ * @param {object} [options] - fetch options (headers, body, etc.)
136
+ * @param {object} [params] - ParamCollection for handleHttpError context
137
+ * @returns {Promise<Response>}
138
+ */
139
+ async function mtxFetch(method, url, options = {}, params) {
140
+ let response;
141
+ try {
142
+ response = await fetch(url, { method, ...options });
143
+ } catch (e) {
144
+ throw buildNetworkError(method, url, e);
145
+ }
146
+ if (!response.ok) {
147
+ handleHttpError(await buildResponseError(method, url, response), params, { url });
148
+ }
149
+ return response;
150
+ }
151
+
152
+ module.exports = {
153
+ buildError,
154
+ buildResponseError,
155
+ buildNetworkError,
156
+ parseResponseBody,
157
+ mtxFetch
158
+ };
package/lib/util/cf.js CHANGED
@@ -4,8 +4,6 @@ const os = require('os');
4
4
  const path = require('path');
5
5
  const util = require('util');
6
6
 
7
- const axios = require('axios');
8
-
9
7
  const IS_WIN = os.platform() === 'win32';
10
8
  const execAsync = util.promisify(cp.exec);
11
9
 
@@ -29,7 +27,6 @@ const OPERATION_STATE_FAILED = 'failed';
29
27
  const OPERATION_STATE_SUCCEEDED = 'succeeded';
30
28
 
31
29
  const CF_CLIENT_MINIMUM_VERSION = 8;
32
- const ENCODED_COMMA = encodeURIComponent(',');
33
30
 
34
31
  module.exports = new class CfUtil {
35
32
 
@@ -68,42 +65,46 @@ module.exports = new class CfUtil {
68
65
  }
69
66
 
70
67
  async curl(urlPath, queryObj, bodyObj) {
71
- const url = this.#target.apiEndpoint + urlPath;
68
+ let url = this.#target.apiEndpoint + urlPath
72
69
 
73
- const options = {
74
- headers: { 'Authorization': this.#target.token }
75
- };
70
+ const headers = { 'Authorization': this.#target.token }
76
71
 
77
72
  if (queryObj && Object.keys(queryObj).length > 0) {
78
- const params = {};
73
+ const parts = []
79
74
  for (const [k, v] of Object.entries(queryObj)) {
80
- // cf rest API does not accept comma in query parameters
81
- params[k] = v.replaceAll(',', ENCODED_COMMA);
75
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
82
76
  }
83
- options.params = params;
77
+ url += '?' + parts.join('&')
84
78
  }
85
79
 
86
- try {
87
- let response;
88
- if (bodyObj) {
89
- response = (await axios.post(url, bodyObj, options));
90
- } else {
91
- response = (await axios.get(url, options));
92
- }
93
- if (LOG._debug) {
94
- options.headers.Authorization = 'bearer <token>';
95
- LOG.debug(`POST ${url}\n${JSON.stringify(options, null, 4)}\n${JSON.stringify(response.data, null, 4)}`);
96
- }
80
+ const fetchOptions = { headers }
81
+ if (bodyObj) {
82
+ fetchOptions.method = 'POST'
83
+ fetchOptions.headers['Content-Type'] = 'application/json'
84
+ fetchOptions.body = JSON.stringify(bodyObj)
85
+ }
97
86
 
98
- return response.data;
99
- } catch (err) {
100
- if (err instanceof axios.AxiosError) {
101
- if (err.response?.data?.errors[0]) {
102
- throw JSON.stringify(err.response.data.errors[0], null, 4);
103
- }
104
- throw `${err.name} ${err.code} ${err.status||''}: ${err.message} while accessing ${url}`;
87
+ const response = await fetch(url, fetchOptions)
88
+
89
+ if (!response.ok) {
90
+ const text = await response.text()
91
+ let data
92
+ try { data = JSON.parse(text) } catch { /* ignore */ }
93
+ if (data?.errors?.[0]) {
94
+ throw JSON.stringify(data.errors[0], null, 2)
105
95
  }
96
+ throw `${response.status} ${response.statusText}: while accessing ${url}`
97
+ }
98
+
99
+ const text = await response.text()
100
+ const data = text ? JSON.parse(text) : {}
101
+
102
+ if (LOG._debug) {
103
+ const method = fetchOptions.method || 'GET'
104
+ LOG.debug(`${method} ${url}\n${JSON.stringify({ headers: { ...headers, Authorization: 'bearer <token>' } }, null, 4)}\n${JSON.stringify(data, null, 4)}`)
106
105
  }
106
+
107
+ return data
107
108
  }
108
109
 
109
110
  _extract(string, pattern, errorMsg) {
@@ -31,7 +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
+ // env.CDS_WATCH_HOT_RELOAD = options['experimental-hot-reload']
35
35
  child = fork (script, { cwd, env, stdio:'inherit', execArgv }, (err)=>{
36
36
  if (err) console.error (err)
37
37
  })
@@ -0,0 +1,76 @@
1
+ const cds = require('../../lib/cds')
2
+ const { pathToFileURL } = require('node:url')
3
+ const { path, stat, readdir, isdir, colors: { BOLD, RESET } } = cds.utils
4
+
5
+ let hmrPort = 24100
6
+
7
+ async function hasViteConfig(appPath) {
8
+ for (const name of ['vite.config.js', 'vite.config.mjs', 'vite.config.ts']) {
9
+ const file = await stat(path.join(appPath, name)).catch(e => e.code === 'ENOENT' ? null : Promise.reject(e))
10
+ return !!file
11
+ }
12
+ return false
13
+ }
14
+
15
+ async function findViteApps() {
16
+ const app = cds.env.folders.app
17
+ const apps = []
18
+ if (!isdir(app)) return apps
19
+ const entries = await readdir(app)
20
+ for (const entry of entries) {
21
+ const appName = typeof entry === 'string' ? entry : entry.name
22
+ if (!appName) continue
23
+ const appPath = path.join(cds.root, app, appName)
24
+ if (!isdir(appPath)) continue
25
+ if (await hasViteConfig(appPath)) apps.push({ name: appName, path: appPath })
26
+ }
27
+ return apps
28
+ }
29
+
30
+ async function loadCreateServer(fromPath) {
31
+ try {
32
+ const vitePath = require.resolve('vite', { paths: [fromPath] })
33
+ return (await import(pathToFileURL(vitePath).href)).createServer
34
+ } catch(e) {
35
+ if (e.code !== 'MODULE_NOT_FOUND') throw e
36
+ /* try next path */
37
+ }
38
+ const relative = path.relative(cds.root, fromPath)
39
+ throw `[cds] - Found Vite app in ${relative}, but package "vite" is not installed. Run ${BOLD}npm install vite --prefix ${relative}${RESET} to install it.`
40
+ }
41
+
42
+ cds.on('bootstrap', app => {
43
+
44
+ let viteMiddleware = null
45
+ app.use((req, res, next) => {
46
+ if (viteMiddleware) return viteMiddleware(req, res, next)
47
+ next()
48
+ })
49
+
50
+ cds.on('served', async () => {
51
+ const viteApps = await findViteApps()
52
+ if (!viteApps.length) return
53
+
54
+ const createServer = await loadCreateServer(viteApps[0].path)
55
+
56
+ // Build a middleware chain from all Vite servers, keyed by base path
57
+ const viteServers = []
58
+ const normalizeBase = base => {
59
+ if (!base.startsWith('/')) base = `/${base}`
60
+ return base.endsWith('/') ? base : `${base}/`
61
+ }
62
+
63
+ for (const viteApp of viteApps) {
64
+ const base = normalizeBase(viteApp.name)
65
+ const server = await createServer({ base, root: viteApp.path, server: { middlewareMode: true, hmr: { port: hmrPort++ } }})
66
+ viteServers.push({ base, middlewares: server.middlewares })
67
+ }
68
+
69
+ viteMiddleware = (req, res, next) => {
70
+ const url = req.url || ''
71
+ const match = viteServers.find(v => url.startsWith(v.base) || url.startsWith('/@'))
72
+ if (match) return match.middlewares(req, res, next)
73
+ next()
74
+ }
75
+ })
76
+ })
@@ -8,14 +8,15 @@
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
+ require('./vite-watch')
11
12
  const bootstrap = require('../../bin/cds')(...args)
12
13
 
13
14
  cds.once('listening', async ({ server }) => {
14
15
  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
- }
16
+ // if (process.env.CDS_WATCH_HOT_RELOAD) {
17
+ // const hotReload = require('./hot-reload');
18
+ // await hotReload()
19
+ // }
19
20
  })
20
21
 
21
22
  try {