ui5-test-runner 5.13.1 → 6.0.0-beta.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.
Files changed (186) hide show
  1. package/README.md +3 -2
  2. package/dist/Npm.js +80 -0
  3. package/dist/browsers/IBrowser.js +1 -0
  4. package/dist/browsers/factory.js +9 -0
  5. package/dist/browsers/puppeteer.js +158 -0
  6. package/dist/cli.js +17 -0
  7. package/dist/configuration/CommandLine.js +112 -0
  8. package/dist/configuration/Configuration.js +1 -0
  9. package/dist/configuration/ConfigurationValidator.js +79 -0
  10. package/dist/configuration/Option.js +1 -0
  11. package/dist/configuration/OptionValidationError.js +15 -0
  12. package/dist/configuration/indexedOptions.js +13 -0
  13. package/dist/configuration/options.js +191 -0
  14. package/dist/configuration/validators/OptionValidator.js +1 -0
  15. package/dist/configuration/validators/boolean.js +15 -0
  16. package/dist/configuration/validators/browser.js +11 -0
  17. package/dist/configuration/validators/fsEntry.js +70 -0
  18. package/dist/configuration/validators/index.js +20 -0
  19. package/dist/configuration/validators/integer.js +10 -0
  20. package/dist/configuration/validators/percent.js +17 -0
  21. package/dist/configuration/validators/regexp.js +20 -0
  22. package/dist/configuration/validators/string.js +7 -0
  23. package/dist/configuration/validators/timeout.js +24 -0
  24. package/dist/configuration/validators/url.js +8 -0
  25. package/dist/modes/ModeFunction.js +1 -0
  26. package/dist/modes/Modes.js +9 -0
  27. package/dist/modes/execute.js +27 -0
  28. package/dist/modes/help.js +3 -0
  29. package/dist/modes/log/ILogStorage.js +1 -0
  30. package/dist/modes/log/LogMetrics.js +9 -0
  31. package/dist/modes/log/LogReader.js +37 -0
  32. package/dist/modes/log/LogStorage.js +68 -0
  33. package/dist/modes/log/REserve.js +101 -0
  34. package/dist/modes/log/index.js +58 -0
  35. package/dist/modes/test/REserve.js +31 -0
  36. package/dist/modes/test/agent.js +8 -0
  37. package/dist/modes/test/browser.js +37 -0
  38. package/dist/modes/test/index.js +66 -0
  39. package/dist/modes/test/pageTask.js +145 -0
  40. package/dist/modes/test/report.js +3 -0
  41. package/dist/modes/test/server.js +109 -0
  42. package/dist/modes/version.js +11 -0
  43. package/dist/platform/Exit.js +139 -0
  44. package/dist/platform/FileSystem.js +13 -0
  45. package/dist/platform/Host.js +10 -0
  46. package/dist/platform/Http.js +38 -0
  47. package/dist/platform/Path.js +5 -0
  48. package/dist/platform/Process.js +133 -0
  49. package/dist/platform/Terminal.js +47 -0
  50. package/dist/platform/Thread.js +43 -0
  51. package/dist/platform/ZLib.js +7 -0
  52. package/dist/platform/assert.js +17 -0
  53. package/dist/platform/constants.js +5 -0
  54. package/dist/platform/environment.js +28 -0
  55. package/dist/platform/index.js +13 -0
  56. package/dist/platform/logger/ILogger.js +1 -0
  57. package/dist/platform/logger/allCompressed.js +54 -0
  58. package/dist/platform/logger/compress.js +277 -0
  59. package/dist/platform/logger/output/BaseLoggerOutput.js +158 -0
  60. package/dist/platform/logger/output/InteractiveLoggerOutput.js +102 -0
  61. package/dist/platform/logger/output/StaticLoggerOutput.js +32 -0
  62. package/dist/platform/logger/output/factory.js +10 -0
  63. package/dist/platform/logger/output.js +58 -0
  64. package/dist/platform/logger/proxy.js +6 -0
  65. package/dist/platform/logger/toInternalLogAttributes.js +22 -0
  66. package/dist/platform/logger/types.js +7 -0
  67. package/dist/platform/logger.js +138 -0
  68. package/dist/platform/mock.js +104 -0
  69. package/dist/platform/version.js +8 -0
  70. package/dist/platform/workerBootstrap.js +21 -0
  71. package/dist/reports/html.js +46 -0
  72. package/dist/types/AgentState.js +1 -0
  73. package/dist/types/CommonTestReportFormat.js +50 -0
  74. package/dist/types/IError.js +1 -0
  75. package/dist/types/IUserInterfaceController.js +1 -0
  76. package/dist/types/typeUtilities.js +1 -0
  77. package/dist/ui/agent.js +3 -0
  78. package/dist/ui/html-report.js +2 -0
  79. package/dist/ui/lib.js +1 -0
  80. package/dist/ui/log-viewer.js +2 -0
  81. package/dist/utils/node/Folder.js +28 -0
  82. package/dist/utils/node/FramedStreamReader.js +86 -0
  83. package/dist/utils/node/FramedStreamWriter.js +27 -0
  84. package/dist/utils/shared/ProgressBar.js +43 -0
  85. package/dist/utils/shared/TestReportBuilder.js +48 -0
  86. package/dist/utils/shared/memoize.js +19 -0
  87. package/dist/utils/shared/object.js +8 -0
  88. package/dist/utils/shared/parallelize.js +59 -0
  89. package/dist/utils/shared/string.js +23 -0
  90. package/dist/utils/shared/toIError.js +17 -0
  91. package/package.json +73 -50
  92. package/.releaserc +0 -5
  93. package/index.js +0 -175
  94. package/jest.config.json +0 -31
  95. package/src/add-test-pages.js +0 -67
  96. package/src/batch.js +0 -214
  97. package/src/browsers.js +0 -319
  98. package/src/capabilities/index.js +0 -204
  99. package/src/capabilities/tests/basic/iframe.html +0 -8
  100. package/src/capabilities/tests/basic/index.html +0 -12
  101. package/src/capabilities/tests/basic/index.js +0 -20
  102. package/src/capabilities/tests/basic/ui5.html +0 -24
  103. package/src/capabilities/tests/dynamic-include/index.js +0 -21
  104. package/src/capabilities/tests/dynamic-include/mix.html +0 -11
  105. package/src/capabilities/tests/dynamic-include/one.html +0 -11
  106. package/src/capabilities/tests/dynamic-include/post.js +0 -3
  107. package/src/capabilities/tests/dynamic-include/test.js +0 -1
  108. package/src/capabilities/tests/dynamic-include/two.html +0 -11
  109. package/src/capabilities/tests/index.js +0 -16
  110. package/src/capabilities/tests/local-storage/index.html +0 -16
  111. package/src/capabilities/tests/local-storage/index.js +0 -21
  112. package/src/capabilities/tests/screenshot/index.html +0 -23
  113. package/src/capabilities/tests/screenshot/index.js +0 -24
  114. package/src/capabilities/tests/scripts/coverage.html +0 -32
  115. package/src/capabilities/tests/scripts/iframe.html +0 -18
  116. package/src/capabilities/tests/scripts/index.js +0 -59
  117. package/src/capabilities/tests/scripts/qunit.html +0 -22
  118. package/src/capabilities/tests/scripts/testsuite.html +0 -10
  119. package/src/capabilities/tests/scripts/testsuite.js +0 -8
  120. package/src/capabilities/tests/timeout/index.html +0 -21
  121. package/src/capabilities/tests/timeout/index.js +0 -19
  122. package/src/capabilities/tests/traces/index.html +0 -18
  123. package/src/capabilities/tests/traces/index.js +0 -81
  124. package/src/capabilities/tests/ui5/focus.html +0 -89
  125. package/src/capabilities/tests/ui5/index.js +0 -39
  126. package/src/capabilities/tests/ui5/language.html +0 -50
  127. package/src/capabilities/tests/ui5/timezone.html +0 -27
  128. package/src/clean.js +0 -22
  129. package/src/cors.js +0 -21
  130. package/src/coverage.js +0 -384
  131. package/src/csv-reader.js +0 -36
  132. package/src/csv-writer.js +0 -55
  133. package/src/defaults/.nycrc.json +0 -4
  134. package/src/defaults/browser.js +0 -217
  135. package/src/defaults/happy-dom.js +0 -123
  136. package/src/defaults/jsdom/compatibility.js +0 -163
  137. package/src/defaults/jsdom/debug.js +0 -23
  138. package/src/defaults/jsdom/resource-loader.js +0 -44
  139. package/src/defaults/jsdom/sap.ui.test.matchers.visible.js +0 -39
  140. package/src/defaults/jsdom.js +0 -95
  141. package/src/defaults/json-report.js +0 -36
  142. package/src/defaults/junit-xml-report.js +0 -90
  143. package/src/defaults/playwright.js +0 -142
  144. package/src/defaults/puppeteer.js +0 -124
  145. package/src/defaults/report/common.js +0 -38
  146. package/src/defaults/report/decompress.js +0 -19
  147. package/src/defaults/report/default.html +0 -99
  148. package/src/defaults/report/main.js +0 -69
  149. package/src/defaults/report/progress.js +0 -60
  150. package/src/defaults/report/styles.css +0 -66
  151. package/src/defaults/report.js +0 -91
  152. package/src/defaults/scan-ui5.js +0 -26
  153. package/src/defaults/selenium-webdriver/chrome.js +0 -39
  154. package/src/defaults/selenium-webdriver/edge.js +0 -24
  155. package/src/defaults/selenium-webdriver/firefox.js +0 -30
  156. package/src/defaults/selenium-webdriver.js +0 -129
  157. package/src/defaults/text-report.js +0 -108
  158. package/src/defaults/webdriverio.js +0 -80
  159. package/src/end.js +0 -62
  160. package/src/endpoints.js +0 -219
  161. package/src/error.js +0 -54
  162. package/src/get-job-progress.js +0 -78
  163. package/src/handle.js +0 -43
  164. package/src/if.js +0 -10
  165. package/src/inject/jest2qunit.js +0 -289
  166. package/src/inject/opa-iframe-coverage.js +0 -22
  167. package/src/inject/post.js +0 -141
  168. package/src/inject/qunit-hooks.js +0 -107
  169. package/src/inject/qunit-redirect.js +0 -65
  170. package/src/inject/ui5-coverage.js +0 -33
  171. package/src/job-mode.js +0 -65
  172. package/src/job.js +0 -493
  173. package/src/npm.js +0 -136
  174. package/src/options.js +0 -95
  175. package/src/output.js +0 -739
  176. package/src/parallelize.js +0 -63
  177. package/src/qunit-hooks.js +0 -219
  178. package/src/report.js +0 -89
  179. package/src/reserve.js +0 -25
  180. package/src/start.js +0 -133
  181. package/src/symbols.js +0 -8
  182. package/src/tests.js +0 -183
  183. package/src/timeout.js +0 -53
  184. package/src/tools.js +0 -179
  185. package/src/ui5.js +0 -199
  186. package/src/unhandled.js +0 -32
@@ -0,0 +1,58 @@
1
+ import { __developmentMode, Exit } from '../../platform/index.js';
2
+ import { LogReader } from './LogReader.js';
3
+ import { serve } from 'reserve';
4
+ import { LogStorage } from './LogStorage.js';
5
+ import { buildREserveConfiguration } from './REserve.js';
6
+ import { BrowserFactory } from '../../browsers/factory.js';
7
+ import { getInitialLogMetrics } from './LogMetrics.js';
8
+ export const log = async (configuration) => {
9
+ const logFileName = configuration.log;
10
+ let stopped = false;
11
+ const metrics = getInitialLogMetrics();
12
+ const storage = LogStorage.create();
13
+ const { promise, resolve } = Promise.withResolvers();
14
+ const abortController = new AbortController();
15
+ const abortSignal = abortController.signal;
16
+ const server = serve(buildREserveConfiguration(storage, metrics, abortController));
17
+ const stop = async () => {
18
+ stopped = true;
19
+ await server?.close();
20
+ resolve();
21
+ };
22
+ abortSignal.addEventListener('abort', () => {
23
+ void stop();
24
+ });
25
+ Exit.registerAsyncTask({
26
+ name: 'log',
27
+ stop: stop
28
+ });
29
+ const browser = await BrowserFactory.build('puppeteer');
30
+ const browserReady = browser.setup({
31
+ visible: true
32
+ });
33
+ server.on('ready', ({ url, port }) => {
34
+ console.log(url);
35
+ void browserReady
36
+ .then(() => browser.newWindow({
37
+ pageId: 0,
38
+ url: `http://localhost:${port}/`,
39
+ scripts: []
40
+ }))
41
+ .then(() => {
42
+ console.log('Use CTRL+C to exit');
43
+ });
44
+ });
45
+ for await (const item of LogReader.read(logFileName, abortSignal)) {
46
+ if (stopped) {
47
+ break;
48
+ }
49
+ const { type, ...attributes } = item;
50
+ if (type === 'log') {
51
+ storage.add(attributes);
52
+ }
53
+ else if (__developmentMode) {
54
+ Object.assign(metrics, attributes);
55
+ }
56
+ }
57
+ await promise;
58
+ };
@@ -0,0 +1,31 @@
1
+ import { logger } from '../../platform/index.js';
2
+ export const buildREserveConfiguration = (configuration) => {
3
+ const match = /\/((?:test-)?resources\/.*)/;
4
+ let { ui5 } = configuration;
5
+ if (!ui5.endsWith('/')) {
6
+ ui5 += '/';
7
+ }
8
+ const mappingUrl = new URL('$1', ui5).toString();
9
+ return {
10
+ port: configuration.port ?? 0,
11
+ mappings: [
12
+ {
13
+ method: 'GET,HEAD',
14
+ match,
15
+ url: mappingUrl,
16
+ 'ignore-unverifiable-certificate': true
17
+ },
18
+ {
19
+ match: /^\/(.*)/,
20
+ cwd: configuration.webapp,
21
+ file: '$1'
22
+ },
23
+ {
24
+ custom: (request) => logger.debug({ source: 'server', message: 'Unhandled request', data: { url: request.url } })
25
+ },
26
+ {
27
+ status: 404
28
+ }
29
+ ]
30
+ };
31
+ };
@@ -0,0 +1,8 @@
1
+ import { __developmentMode, __sourcesRoot, Path, FileSystem } from '../../platform/index.js';
2
+ import { memoize } from '../../utils/shared/memoize.js';
3
+ export const getAgentSource = memoize(async () => {
4
+ const path = __developmentMode
5
+ ? Path.join(__sourcesRoot, '../dist/ui', 'agent.js')
6
+ : Path.join(__sourcesRoot, 'ui/agent.js');
7
+ return FileSystem.readFile(path, 'utf8');
8
+ });
@@ -0,0 +1,37 @@
1
+ import { BrowserFactory } from '../../browsers/factory.js';
2
+ import { Exit, assert, logger } from '../../platform/index.js';
3
+ let browser;
4
+ export const setupBrowser = async (configuration) => {
5
+ assert(configuration.browser === 'puppeteer');
6
+ browser = await BrowserFactory.build('puppeteer');
7
+ const { debugKeepBrowserOpen } = configuration;
8
+ const settings = {
9
+ visible: debugKeepBrowserOpen
10
+ };
11
+ try {
12
+ await browser.setup(settings);
13
+ }
14
+ catch (error) {
15
+ logger.fatal({ source: 'job', message: 'Unable to setup browser', error });
16
+ throw error;
17
+ }
18
+ if (debugKeepBrowserOpen) {
19
+ const { newWindow } = browser;
20
+ browser.newWindow = async (settings) => {
21
+ const window = await newWindow.call(browser, settings);
22
+ window.close = async () => { };
23
+ return window;
24
+ };
25
+ const { promise, resolve } = Promise.withResolvers();
26
+ Exit.registerAsyncTask({
27
+ name: 'debugKeepBrowserOpen',
28
+ stop: () => resolve()
29
+ });
30
+ browser.shutdown = () => {
31
+ logger.warn({ source: 'job', message: 'Browser will remain open, use CTRL+C to end command' });
32
+ return promise;
33
+ };
34
+ }
35
+ return browser;
36
+ };
37
+ export const getBrowser = () => browser;
@@ -0,0 +1,66 @@
1
+ import { logger, logEnvironnement, Exit, FileSystem, Path, Http } from '../../platform/index.js';
2
+ import { defaults } from '../../configuration/options.js';
3
+ import { parallelize } from '../../utils/shared/parallelize.js';
4
+ import { getAgentSource } from './agent.js';
5
+ import { setupBrowser } from './browser.js';
6
+ import { pageTask } from './pageTask.js';
7
+ import { reportBuilder } from './report.js';
8
+ import { generateHtmlReport } from '../../reports/html.js';
9
+ import { Folder } from '../../utils/node/Folder.js';
10
+ import { server } from './server.js';
11
+ import { formatDuration } from '../../utils/shared/string.js';
12
+ export const test = async (configuration) => {
13
+ await Folder.create(configuration.reportDir);
14
+ logger.start(configuration);
15
+ logger.debug({ source: 'job', message: 'Configuration', data: { defaults, configuration } });
16
+ await logEnvironnement();
17
+ await getAgentSource();
18
+ let browser;
19
+ try {
20
+ const port = await server.start(configuration);
21
+ const version = JSON.parse(await Http.get(`http://localhost:${port}/resources/sap-ui-version.json`));
22
+ const { version: coreVersion } = version.libraries.find(({ name }) => name === 'sap.ui.core') ?? {
23
+ version: 'unknown'
24
+ };
25
+ logger.info({ source: 'job', message: `UI5 version used by the local server: ${coreVersion}` });
26
+ if (!configuration.url) {
27
+ configuration.url = [new URL(configuration.testsuite, `http://localhost:0`).toString()];
28
+ }
29
+ const urls = [...configuration.url.map((url) => url.replace(':0/', `:${port}/`))];
30
+ browser = await setupBrowser(configuration);
31
+ logger.info({ source: 'progress', message: 'Executing pages', pageId: undefined, data: { value: 0, max: 0 } });
32
+ let completed = 0;
33
+ await parallelize(pageTask, urls, {
34
+ parallel: configuration.parallel,
35
+ on: (event) => {
36
+ if (event.type === 'failed') {
37
+ logger.error({ source: 'job', message: 'page failed', error: event.error, data: { url: event.input } });
38
+ Exit.code = -1;
39
+ }
40
+ if (event.type === 'completed') {
41
+ ++completed;
42
+ }
43
+ logger.info({
44
+ source: 'progress',
45
+ message: 'Executing pages',
46
+ pageId: undefined,
47
+ data: { value: completed, max: urls.length }
48
+ });
49
+ }
50
+ });
51
+ reportBuilder.finalize();
52
+ FileSystem.writeFileSync(Path.join(configuration.reportDir, 'report.json'), JSON.stringify(reportBuilder.report, undefined, 2), 'utf8');
53
+ await generateHtmlReport(configuration, reportBuilder.report);
54
+ const { duration } = reportBuilder.report.results.summary;
55
+ if (duration) {
56
+ logger.info({ source: 'job', message: `Tests duration: ${formatDuration(duration)}` });
57
+ }
58
+ }
59
+ catch (error) {
60
+ logger.error({ source: 'job', message: 'An error occurred', error });
61
+ }
62
+ finally {
63
+ await browser?.shutdown();
64
+ await server.stop();
65
+ }
66
+ };
@@ -0,0 +1,145 @@
1
+ import { assert, logger } from '../../platform/index.js';
2
+ import { getAgentSource } from './agent.js';
3
+ import { getBrowser } from './browser.js';
4
+ import { Exit, ExitShutdownError } from '../../platform/Exit.js';
5
+ import { setTimeout } from 'node:timers/promises';
6
+ import { reportBuilder } from './report.js';
7
+ let lastPageId = 0;
8
+ const reportQunitProgress = (context, agentState) => {
9
+ if (agentState.isOpa) {
10
+ context.loopDelay = 1000;
11
+ }
12
+ if (agentState.total > 0) {
13
+ const { executed, total, errors } = agentState;
14
+ if (executed !== context.lastExecuted || total !== context.lastTotal) {
15
+ const type = agentState.isOpa ? 'opa' : 'qunit';
16
+ context.lastExecuted = executed;
17
+ context.lastTotal = total;
18
+ context.errors = errors;
19
+ context.type = type;
20
+ logger.info({
21
+ source: 'progress',
22
+ message: context.url,
23
+ pageId: context.pageId,
24
+ data: { max: agentState.total, value: agentState.executed, type, errors }
25
+ });
26
+ }
27
+ }
28
+ };
29
+ const queryAgentState = async (context) => {
30
+ const agentState = (await context.page.eval("window['ui5-test-runner'].state"));
31
+ logger.debug({ source: 'page', message: 'agent state', data: { state: agentState }, pageId: context.pageId });
32
+ if (agentState.done) {
33
+ if (agentState.type === 'suite') {
34
+ for (const page of agentState.pages) {
35
+ const pageUrl = new URL(page, context.url).toString();
36
+ context.urls.push(pageUrl);
37
+ }
38
+ }
39
+ else if (agentState.type === 'unknown') {
40
+ logger.fatal({
41
+ source: 'page',
42
+ message: 'Unable to detect page type',
43
+ pageId: context.pageId,
44
+ data: { state: agentState }
45
+ });
46
+ throw new Error('Unable to detect page type');
47
+ }
48
+ else {
49
+ assert(agentState.type === 'QUnit');
50
+ reportQunitProgress(context, agentState);
51
+ }
52
+ return true;
53
+ }
54
+ if (agentState.type === 'QUnit') {
55
+ reportQunitProgress(context, agentState);
56
+ if (agentState.uncaughtErrors?.length) {
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ };
62
+ export const pageTask = async function (url, index, urls) {
63
+ const pageId = ++lastPageId;
64
+ logger.debug({ source: 'page', message: 'new page task', pageId, data: { url } });
65
+ logger.info({
66
+ source: 'progress',
67
+ message: url,
68
+ pageId,
69
+ data: { max: 0, value: 1, type: 'unknown', errors: 0 }
70
+ });
71
+ const { promise: taskStopped, resolve: setTaskAsStopped } = Promise.withResolvers();
72
+ using _ = Exit.registerAsyncTask({
73
+ name: url,
74
+ stop: async () => {
75
+ try {
76
+ this.stop(new ExitShutdownError());
77
+ }
78
+ catch {
79
+ }
80
+ finally {
81
+ await taskStopped;
82
+ }
83
+ }
84
+ });
85
+ let page;
86
+ let context;
87
+ try {
88
+ const agentSource = await getAgentSource();
89
+ const browser = getBrowser();
90
+ page = await browser.newWindow({
91
+ pageId,
92
+ scripts: [agentSource],
93
+ url
94
+ });
95
+ context = {
96
+ pageId,
97
+ urls,
98
+ url,
99
+ page,
100
+ loopDelay: 250,
101
+ type: 'unknown',
102
+ lastExecuted: 0,
103
+ errors: 0,
104
+ lastTotal: 0
105
+ };
106
+ while (!this.stopRequested) {
107
+ try {
108
+ await setTimeout(context.loopDelay);
109
+ if (await queryAgentState(context)) {
110
+ break;
111
+ }
112
+ }
113
+ catch (error) {
114
+ logger.error({ source: 'page', message: 'An error occurred', error, pageId, data: {} });
115
+ break;
116
+ }
117
+ }
118
+ const testResults = (await page.eval("window['ui5-test-runner'].results"));
119
+ logger.debug({ source: 'page', message: 'test results', pageId, data: { results: testResults } });
120
+ reportBuilder.merge(url, testResults);
121
+ }
122
+ finally {
123
+ if (context !== undefined) {
124
+ logger.info({
125
+ source: 'progress',
126
+ message: url,
127
+ pageId,
128
+ data: {
129
+ max: context.lastTotal,
130
+ value: context.lastExecuted,
131
+ type: context.type,
132
+ errors: context.errors,
133
+ remove: true
134
+ }
135
+ });
136
+ }
137
+ try {
138
+ await page?.close();
139
+ }
140
+ catch (error) {
141
+ logger.error({ source: 'page', message: 'page.close failed', error, pageId, data: {} });
142
+ }
143
+ setTaskAsStopped();
144
+ }
145
+ };
@@ -0,0 +1,3 @@
1
+ import { TestReportBuilder } from '../../utils/shared/TestReportBuilder.js';
2
+ import { version } from '../../platform/version.js';
3
+ export const reportBuilder = new TestReportBuilder(crypto.randomUUID(), await version());
@@ -0,0 +1,109 @@
1
+ import { assert, Exit, logger, Thread } from '../../platform/index.js';
2
+ import { serve } from 'reserve';
3
+ import { toPlainObject } from '../../utils/shared/object.js';
4
+ import { buildREserveConfiguration } from './REserve.js';
5
+ let channel;
6
+ let serverWorker;
7
+ let stopping = false;
8
+ export const server = {
9
+ async start(configuration) {
10
+ assert(serverWorker === undefined);
11
+ channel = Thread.createBroadcastChannel('server');
12
+ Exit.registerAsyncTask({
13
+ name: 'server',
14
+ stop: () => server.stop()
15
+ });
16
+ logger.debug({ source: 'server', message: 'Starting server' });
17
+ serverWorker = Thread.createWorker('modes/test/server', toPlainObject(configuration));
18
+ const { promise, resolve, reject } = Promise.withResolvers();
19
+ channel.onmessage = ({ data: message }) => {
20
+ if (message.command === 'ready') {
21
+ resolve(message.port);
22
+ }
23
+ else if (message.command === 'error') {
24
+ reject(new Error('failed to start'));
25
+ }
26
+ else {
27
+ assert(false, 'unexpected');
28
+ }
29
+ };
30
+ return promise;
31
+ },
32
+ async stop() {
33
+ if (stopping) {
34
+ return;
35
+ }
36
+ stopping = true;
37
+ try {
38
+ assert(serverWorker !== undefined);
39
+ logger.debug({ source: 'server', message: 'Stopping server' });
40
+ channel.postMessage({
41
+ command: 'terminate'
42
+ });
43
+ const { promise, resolve } = Promise.withResolvers();
44
+ channel.onmessage = ({ data: message }) => {
45
+ if (message.command === 'terminated') {
46
+ channel.close();
47
+ resolve();
48
+ }
49
+ };
50
+ await promise;
51
+ }
52
+ finally {
53
+ channel.close();
54
+ }
55
+ }
56
+ };
57
+ export const workerMain = (configuration) => {
58
+ logger.debug({ source: 'server', message: 'Starting server...' });
59
+ channel = Thread.createBroadcastChannel('server');
60
+ channel.onmessage = ({ data: message }) => {
61
+ if (message.command === 'terminate') {
62
+ logger.debug({ source: 'server', message: 'Stopping server...' });
63
+ void server.close().finally(() => {
64
+ logger.debug({ source: 'server', message: 'Server stopped.' });
65
+ channel.postMessage({
66
+ command: 'terminated'
67
+ });
68
+ channel.close();
69
+ });
70
+ }
71
+ };
72
+ let server;
73
+ try {
74
+ server = serve(buildREserveConfiguration(configuration));
75
+ }
76
+ catch (error) {
77
+ logger.error({ source: 'server', message: 'An error occurred while configuring', error });
78
+ channel.postMessage({
79
+ command: 'error'
80
+ });
81
+ return;
82
+ }
83
+ server.on('created', () => {
84
+ logger.debug({ source: 'reserve', message: 'created', data: {} });
85
+ });
86
+ for (const eventName of ['incoming', 'redirecting', 'redirected', 'aborted', 'closed']) {
87
+ server.on(eventName, (event) => {
88
+ const { eventName: message, ...data } = event;
89
+ logger.debug({ source: 'reserve', message, data });
90
+ });
91
+ }
92
+ server
93
+ .on('ready', (event) => {
94
+ const { eventName: message, ...data } = event;
95
+ logger.debug({ source: 'reserve', message, data });
96
+ logger.info({ source: 'server', message: `Server listening on: ${event.url}` });
97
+ channel.postMessage({
98
+ command: 'ready',
99
+ port: event.port
100
+ });
101
+ })
102
+ .on('error', (event) => {
103
+ const { eventName: message, reason: error, ...data } = event;
104
+ logger.debug({ source: 'reserve', message, data, error });
105
+ channel.postMessage({
106
+ command: 'error'
107
+ });
108
+ });
109
+ };
@@ -0,0 +1,11 @@
1
+ import { FileSystem } from '../platform/index.js';
2
+ import { Npm } from '../Npm.js';
3
+ export const version = async () => {
4
+ const packageFile = await FileSystem.readFile('package.json', 'utf8');
5
+ const { name, version: installedVersion } = JSON.parse(packageFile);
6
+ console.log(`${name}@${installedVersion}`);
7
+ const latestVersion = await Npm.getLatestVersion(name);
8
+ if (latestVersion !== installedVersion) {
9
+ console.log(`Latest version of ${name} is ${latestVersion}`);
10
+ }
11
+ };
@@ -0,0 +1,139 @@
1
+ import { ServerResponse, ClientRequest } from 'node:http';
2
+ import { Thread } from './Thread.js';
3
+ import { assert } from './assert.js';
4
+ import { logger } from './logger/proxy.js';
5
+ export class ExitShutdownError extends Error {
6
+ constructor() {
7
+ super('Exiting application');
8
+ this.name = 'ExitShutdownError';
9
+ }
10
+ }
11
+ const socketHandleDescriptor = (handle) => {
12
+ if (handle._httpMessage instanceof ServerResponse) {
13
+ const { method, url } = handle._httpMessage.req;
14
+ return `IncomingRequest ${method} ${url}`;
15
+ }
16
+ if (handle._httpMessage instanceof ClientRequest) {
17
+ const { path, method, host, protocol } = handle._httpMessage;
18
+ return `ClientRequest ${method} ${protocol}//${host}${path}`;
19
+ }
20
+ if (handle.localAddress) {
21
+ const { localAddress, localPort, remoteAddress, remotePort } = handle;
22
+ return remoteAddress === undefined
23
+ ? `local ${localAddress}:${localPort}`
24
+ : `local ${localAddress}:${localPort} <-> remote ${remoteAddress}:${remotePort}`;
25
+ }
26
+ if (handle._handle) {
27
+ const underlyingHandle = handle._handle;
28
+ const underlyingClassName = underlyingHandle && underlyingHandle.constructor && underlyingHandle.constructor.name;
29
+ return `${underlyingClassName || 'handle unknown'}`;
30
+ }
31
+ return 'unknown';
32
+ };
33
+ const handleDescriptors = {
34
+ ChildProcess: (handle) => {
35
+ return (`pid: ${handle.pid}` +
36
+ (handle.spawnargs ? ` ${handle.spawnargs.map((value) => ('' + value).replaceAll(' ', '␣'))}` : ' unknown'));
37
+ },
38
+ ReadStream: (handle) => (handle.fd === 0 ? `stdin isTTY: ${handle.isTTY}` : `fd: ${handle.fd}`),
39
+ Server: (handle) => `connections: ${handle._connections} events: ${handle._eventsCount}`,
40
+ Socket: socketHandleDescriptor,
41
+ TLSSocket: socketHandleDescriptor,
42
+ WriteStream: (handle) => handle.fd < 3
43
+ ? `${[0, 'stdout', 'stderr'][handle.fd]} ${handle.columns}x${handle.rows} isTTY: ${handle.isTTY}`
44
+ : `fd: ${handle.fd}`
45
+ };
46
+ const isStdStream = (handle) => 'fd' in handle && handle.fd >= 0 && handle.fd < 3;
47
+ const unknownHandleDescriptor = () => 'unknown';
48
+ const describeHandle = (handle) => {
49
+ const className = handle && handle.constructor && handle.constructor.name;
50
+ return { className, label: (handleDescriptors[className] ?? unknownHandleDescriptor)(handle) };
51
+ };
52
+ export class Exit {
53
+ static _asyncTaskId = 0;
54
+ static _asyncTasks = [];
55
+ static _checkForHandlesLeak() {
56
+ const undocumentedProcess = process;
57
+ if (!undocumentedProcess._getActiveHandles) {
58
+ logger?.warn({ source: 'exit/handle', message: 'Missing process._getActiveHandles' });
59
+ return;
60
+ }
61
+ const activeHandles = undocumentedProcess._getActiveHandles();
62
+ let messagePortFound = false;
63
+ for (const handle of activeHandles) {
64
+ const { className, label } = describeHandle(handle);
65
+ if (isStdStream(handle)) {
66
+ logger?.debug({ source: 'exit/handle', message: `${className} ${label}` });
67
+ }
68
+ else if (className === 'MessagePort' && !messagePortFound) {
69
+ messagePortFound = true;
70
+ logger?.debug({ source: 'exit/handle', message: `${className} ${label}` });
71
+ }
72
+ else {
73
+ logger?.warn({ source: 'exit/handle', message: `possible leak ${className} ${label}` });
74
+ if (className === 'TLSSocket' || className === 'Socket') {
75
+ handle.destroy();
76
+ }
77
+ }
78
+ }
79
+ }
80
+ static set code(code) {
81
+ assert(Thread.isMainThread, 'Exit.code can be set only on main thread');
82
+ process.exitCode = code;
83
+ }
84
+ static registerAsyncTask(task) {
85
+ assert(Thread.isMainThread, 'Exit.registerAsyncTask can be called only on main thread');
86
+ if (Exit._enteringShutdown) {
87
+ throw new ExitShutdownError();
88
+ }
89
+ const id = ++Exit._asyncTaskId;
90
+ this._asyncTasks.push({
91
+ id,
92
+ ...task
93
+ });
94
+ return {
95
+ [Symbol.dispose]() {
96
+ const index = Exit._asyncTasks.findIndex((task) => task.id === id);
97
+ try {
98
+ assert(index !== -1, 'unable to find Exit async task to unregister');
99
+ Exit._asyncTasks.splice(index, 1);
100
+ }
101
+ catch {
102
+ }
103
+ }
104
+ };
105
+ }
106
+ static _enteringShutdown = false;
107
+ static _logLevel = 'debug';
108
+ static async shutdown() {
109
+ assert(Thread.isMainThread, 'Exit.shutdown can be called only on main thread');
110
+ Exit._enteringShutdown = true;
111
+ const logLevel = Exit._logLevel;
112
+ while (Exit._asyncTasks.length > 0) {
113
+ const task = Exit._asyncTasks.at(-1);
114
+ try {
115
+ logger?.[logLevel]({ source: 'exit', message: `Stopping ${task.name}...` });
116
+ await task.stop();
117
+ logger?.[logLevel]({ source: 'exit', message: `${task.name} stopped.` });
118
+ }
119
+ catch (error) {
120
+ logger?.[logLevel]({ source: 'exit', message: `Failed while stopping ${task.name}...`, error });
121
+ }
122
+ finally {
123
+ if (task === Exit._asyncTasks.at(-1)) {
124
+ Exit._asyncTasks.pop();
125
+ }
126
+ }
127
+ }
128
+ Exit._checkForHandlesLeak();
129
+ logger?.[logLevel]({ source: 'exit', message: `Stopping logger...` });
130
+ await logger?.stop();
131
+ }
132
+ static sigInt() {
133
+ Exit._logLevel = 'info';
134
+ void Exit.shutdown();
135
+ }
136
+ }
137
+ if (Thread.isMainThread) {
138
+ process.on('SIGINT', Exit.sigInt);
139
+ }
@@ -0,0 +1,13 @@
1
+ import { access, stat, constants, readFile, mkdir, rm } from 'node:fs/promises';
2
+ import { createReadStream, createWriteStream, writeFileSync } from 'node:fs';
3
+ export class FileSystem {
4
+ static access = access;
5
+ static constants = constants;
6
+ static createReadStream = createReadStream;
7
+ static createWriteStream = createWriteStream;
8
+ static mkdir = mkdir;
9
+ static readFile = readFile;
10
+ static rm = rm;
11
+ static stat = stat;
12
+ static writeFileSync = writeFileSync;
13
+ }
@@ -0,0 +1,10 @@
1
+ import { machine, cpus, platform } from 'node:os';
2
+ export class Host {
3
+ static cpus = cpus;
4
+ static cwd = process.cwd.bind(process);
5
+ static machine = machine;
6
+ static memoryUsage = process.memoryUsage.bind(process);
7
+ static nodeVersion = process.version;
8
+ static pid = process.pid;
9
+ static platform = platform;
10
+ }
@@ -0,0 +1,38 @@
1
+ import { logger } from './logger.js';
2
+ import { Exit } from './Exit.js';
3
+ let lastRequestId = 0;
4
+ export const Http = {
5
+ async get(url) {
6
+ const requestId = ++lastRequestId;
7
+ const controller = new AbortController();
8
+ using _ = Exit.registerAsyncTask({
9
+ name: `http.get#${requestId}`,
10
+ stop: () => controller.abort()
11
+ });
12
+ logger.debug({ source: 'http', message: `GET ${url}`, data: { requestId } });
13
+ try {
14
+ const response = await fetch(url, {
15
+ signal: controller.signal
16
+ });
17
+ const headers = {};
18
+ for (const [name, value] of response.headers) {
19
+ headers[name] = value;
20
+ }
21
+ logger.debug({
22
+ source: 'http',
23
+ message: `${response.status} ${response.statusText}`,
24
+ data: { requestId, status: response.status, headers }
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error('HTTP request failed');
28
+ }
29
+ const text = await response.text();
30
+ logger.debug({ source: 'http', message: text, data: { requestId } });
31
+ return text;
32
+ }
33
+ catch (error) {
34
+ logger.debug({ source: 'http', message: 'error caught', data: { requestId }, error });
35
+ throw error;
36
+ }
37
+ }
38
+ };