ui5-test-runner 5.13.0 → 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.
- package/README.md +3 -2
- package/dist/Npm.js +80 -0
- package/dist/browsers/IBrowser.js +1 -0
- package/dist/browsers/factory.js +9 -0
- package/dist/browsers/puppeteer.js +158 -0
- package/dist/cli.js +17 -0
- package/dist/configuration/CommandLine.js +112 -0
- package/dist/configuration/Configuration.js +1 -0
- package/dist/configuration/ConfigurationValidator.js +79 -0
- package/dist/configuration/Option.js +1 -0
- package/dist/configuration/OptionValidationError.js +15 -0
- package/dist/configuration/indexedOptions.js +13 -0
- package/dist/configuration/options.js +191 -0
- package/dist/configuration/validators/OptionValidator.js +1 -0
- package/dist/configuration/validators/boolean.js +15 -0
- package/dist/configuration/validators/browser.js +11 -0
- package/dist/configuration/validators/fsEntry.js +70 -0
- package/dist/configuration/validators/index.js +20 -0
- package/dist/configuration/validators/integer.js +10 -0
- package/dist/configuration/validators/percent.js +17 -0
- package/dist/configuration/validators/regexp.js +20 -0
- package/dist/configuration/validators/string.js +7 -0
- package/dist/configuration/validators/timeout.js +24 -0
- package/dist/configuration/validators/url.js +8 -0
- package/dist/modes/ModeFunction.js +1 -0
- package/dist/modes/Modes.js +9 -0
- package/dist/modes/execute.js +27 -0
- package/dist/modes/help.js +3 -0
- package/dist/modes/log/ILogStorage.js +1 -0
- package/dist/modes/log/LogMetrics.js +9 -0
- package/dist/modes/log/LogReader.js +37 -0
- package/dist/modes/log/LogStorage.js +68 -0
- package/dist/modes/log/REserve.js +101 -0
- package/dist/modes/log/index.js +58 -0
- package/dist/modes/test/REserve.js +31 -0
- package/dist/modes/test/agent.js +8 -0
- package/dist/modes/test/browser.js +37 -0
- package/dist/modes/test/index.js +66 -0
- package/dist/modes/test/pageTask.js +145 -0
- package/dist/modes/test/report.js +3 -0
- package/dist/modes/test/server.js +109 -0
- package/dist/modes/version.js +11 -0
- package/dist/platform/Exit.js +139 -0
- package/dist/platform/FileSystem.js +13 -0
- package/dist/platform/Host.js +10 -0
- package/dist/platform/Http.js +38 -0
- package/dist/platform/Path.js +5 -0
- package/dist/platform/Process.js +133 -0
- package/dist/platform/Terminal.js +47 -0
- package/dist/platform/Thread.js +43 -0
- package/dist/platform/ZLib.js +7 -0
- package/dist/platform/assert.js +17 -0
- package/dist/platform/constants.js +5 -0
- package/dist/platform/environment.js +28 -0
- package/dist/platform/index.js +13 -0
- package/dist/platform/logger/ILogger.js +1 -0
- package/dist/platform/logger/allCompressed.js +54 -0
- package/dist/platform/logger/compress.js +277 -0
- package/dist/platform/logger/output/BaseLoggerOutput.js +158 -0
- package/dist/platform/logger/output/InteractiveLoggerOutput.js +102 -0
- package/dist/platform/logger/output/StaticLoggerOutput.js +32 -0
- package/dist/platform/logger/output/factory.js +10 -0
- package/dist/platform/logger/output.js +58 -0
- package/dist/platform/logger/proxy.js +6 -0
- package/dist/platform/logger/toInternalLogAttributes.js +22 -0
- package/dist/platform/logger/types.js +7 -0
- package/dist/platform/logger.js +138 -0
- package/dist/platform/mock.js +104 -0
- package/dist/platform/version.js +8 -0
- package/dist/platform/workerBootstrap.js +21 -0
- package/dist/reports/html.js +46 -0
- package/dist/types/AgentState.js +1 -0
- package/dist/types/CommonTestReportFormat.js +50 -0
- package/dist/types/IError.js +1 -0
- package/dist/types/IUserInterfaceController.js +1 -0
- package/dist/types/typeUtilities.js +1 -0
- package/dist/ui/agent.js +3 -0
- package/dist/ui/html-report.js +2 -0
- package/dist/ui/lib.js +1 -0
- package/dist/ui/log-viewer.js +2 -0
- package/dist/utils/node/Folder.js +28 -0
- package/dist/utils/node/FramedStreamReader.js +86 -0
- package/dist/utils/node/FramedStreamWriter.js +27 -0
- package/dist/utils/shared/ProgressBar.js +43 -0
- package/dist/utils/shared/TestReportBuilder.js +48 -0
- package/dist/utils/shared/memoize.js +19 -0
- package/dist/utils/shared/object.js +8 -0
- package/dist/utils/shared/parallelize.js +59 -0
- package/dist/utils/shared/string.js +23 -0
- package/dist/utils/shared/toIError.js +17 -0
- package/package.json +73 -50
- package/.releaserc +0 -5
- package/index.js +0 -175
- package/jest.config.json +0 -31
- package/src/add-test-pages.js +0 -67
- package/src/batch.js +0 -214
- package/src/browsers.js +0 -319
- package/src/capabilities/index.js +0 -204
- package/src/capabilities/tests/basic/iframe.html +0 -8
- package/src/capabilities/tests/basic/index.html +0 -12
- package/src/capabilities/tests/basic/index.js +0 -20
- package/src/capabilities/tests/basic/ui5.html +0 -24
- package/src/capabilities/tests/dynamic-include/index.js +0 -21
- package/src/capabilities/tests/dynamic-include/mix.html +0 -11
- package/src/capabilities/tests/dynamic-include/one.html +0 -11
- package/src/capabilities/tests/dynamic-include/post.js +0 -3
- package/src/capabilities/tests/dynamic-include/test.js +0 -1
- package/src/capabilities/tests/dynamic-include/two.html +0 -11
- package/src/capabilities/tests/index.js +0 -16
- package/src/capabilities/tests/local-storage/index.html +0 -16
- package/src/capabilities/tests/local-storage/index.js +0 -21
- package/src/capabilities/tests/screenshot/index.html +0 -23
- package/src/capabilities/tests/screenshot/index.js +0 -24
- package/src/capabilities/tests/scripts/coverage.html +0 -32
- package/src/capabilities/tests/scripts/iframe.html +0 -18
- package/src/capabilities/tests/scripts/index.js +0 -59
- package/src/capabilities/tests/scripts/qunit.html +0 -22
- package/src/capabilities/tests/scripts/testsuite.html +0 -10
- package/src/capabilities/tests/scripts/testsuite.js +0 -8
- package/src/capabilities/tests/timeout/index.html +0 -21
- package/src/capabilities/tests/timeout/index.js +0 -19
- package/src/capabilities/tests/traces/index.html +0 -18
- package/src/capabilities/tests/traces/index.js +0 -81
- package/src/capabilities/tests/ui5/focus.html +0 -89
- package/src/capabilities/tests/ui5/index.js +0 -39
- package/src/capabilities/tests/ui5/language.html +0 -50
- package/src/capabilities/tests/ui5/timezone.html +0 -27
- package/src/clean.js +0 -22
- package/src/cors.js +0 -21
- package/src/coverage.js +0 -384
- package/src/csv-reader.js +0 -36
- package/src/csv-writer.js +0 -55
- package/src/defaults/.nycrc.json +0 -4
- package/src/defaults/browser.js +0 -217
- package/src/defaults/happy-dom.js +0 -123
- package/src/defaults/jsdom/compatibility.js +0 -163
- package/src/defaults/jsdom/debug.js +0 -23
- package/src/defaults/jsdom/resource-loader.js +0 -44
- package/src/defaults/jsdom/sap.ui.test.matchers.visible.js +0 -39
- package/src/defaults/jsdom.js +0 -95
- package/src/defaults/json-report.js +0 -36
- package/src/defaults/junit-xml-report.js +0 -90
- package/src/defaults/playwright.js +0 -142
- package/src/defaults/puppeteer.js +0 -124
- package/src/defaults/report/common.js +0 -38
- package/src/defaults/report/decompress.js +0 -19
- package/src/defaults/report/default.html +0 -99
- package/src/defaults/report/main.js +0 -69
- package/src/defaults/report/progress.js +0 -60
- package/src/defaults/report/styles.css +0 -66
- package/src/defaults/report.js +0 -91
- package/src/defaults/scan-ui5.js +0 -26
- package/src/defaults/selenium-webdriver/chrome.js +0 -39
- package/src/defaults/selenium-webdriver/edge.js +0 -24
- package/src/defaults/selenium-webdriver/firefox.js +0 -30
- package/src/defaults/selenium-webdriver.js +0 -129
- package/src/defaults/text-report.js +0 -108
- package/src/defaults/webdriverio.js +0 -80
- package/src/end.js +0 -62
- package/src/endpoints.js +0 -219
- package/src/error.js +0 -54
- package/src/get-job-progress.js +0 -78
- package/src/handle.js +0 -43
- package/src/if.js +0 -10
- package/src/inject/jest2qunit.js +0 -289
- package/src/inject/opa-iframe-coverage.js +0 -22
- package/src/inject/post.js +0 -141
- package/src/inject/qunit-hooks.js +0 -107
- package/src/inject/qunit-redirect.js +0 -65
- package/src/inject/ui5-coverage.js +0 -33
- package/src/job-mode.js +0 -65
- package/src/job.js +0 -493
- package/src/npm.js +0 -136
- package/src/options.js +0 -95
- package/src/output.js +0 -739
- package/src/parallelize.js +0 -63
- package/src/qunit-hooks.js +0 -219
- package/src/report.js +0 -89
- package/src/reserve.js +0 -25
- package/src/start.js +0 -129
- package/src/symbols.js +0 -8
- package/src/tests.js +0 -183
- package/src/timeout.js +0 -53
- package/src/tools.js +0 -179
- package/src/ui5.js +0 -199
- package/src/unhandled.js +0 -32
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# UI5 Test runner
|
|
1
|
+
# UI5 Test runner 6️⃣
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ArnaudBuchholz/ui5-test-runner/actions/workflows/node.js.yml)
|
|
4
4
|
[](https://packagequality.com/#?package=ui5-test-runner)
|
|
@@ -19,7 +19,7 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
|
|
|
19
19
|
|
|
20
20
|
## 💿 How to install
|
|
21
21
|
|
|
22
|
-
* Works with [Node.js](https://nodejs.org/en/download/) >=
|
|
22
|
+
* Works with [Node.js](https://nodejs.org/en/download/) >= 24
|
|
23
23
|
* Local installation
|
|
24
24
|
* `npm install --save-dev ui5-test-runner`
|
|
25
25
|
* Trigger either with `npx ui5-test-runner` or through an npm script invoking `ui5-test-runner`
|
|
@@ -36,6 +36,7 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
|
|
|
36
36
|
|
|
37
37
|
| Version | Reason |
|
|
38
38
|
|-|-|
|
|
39
|
+
| **6**.0.0 | • Drop support of Node.js < 24 |
|
|
39
40
|
| **5**.0.0 | • Some coverage reports now includes **all** files, leading to a potential decrease of coverage |
|
|
40
41
|
| **4**.0.0 | • Drop support of Node.js 16 |
|
|
41
42
|
| **3**.0.0 | • Drop support of Node.js 14 |
|
package/dist/Npm.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { logger, FileSystem, Http, Path, Process } from './platform/index.js';
|
|
2
|
+
import { memoize } from './utils/shared/memoize.js';
|
|
3
|
+
const getNpmCliPath = memoize(async () => {
|
|
4
|
+
const npmChildProcess = Process.spawn('npm', [], {
|
|
5
|
+
shell: true
|
|
6
|
+
});
|
|
7
|
+
await npmChildProcess.closed;
|
|
8
|
+
const error = new Error('Unable to initialize NPM');
|
|
9
|
+
const match = /^npm@([^ ]+) (.*)$/gm.exec(npmChildProcess.stdout);
|
|
10
|
+
if (!match) {
|
|
11
|
+
logger.fatal({ source: 'npm', message: 'Unable to match NPM output', error });
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
const [, semver, path] = match;
|
|
15
|
+
if (!semver || !path) {
|
|
16
|
+
logger.fatal({ source: 'npm', message: 'Failed to parse NPM output', error, data: { semver, path } });
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
logger.debug({ source: 'npm', message: `npm@${semver} ${path}` });
|
|
20
|
+
return Path.join(path, 'bin/npm-cli.js');
|
|
21
|
+
});
|
|
22
|
+
const npm = async (...arguments_) => {
|
|
23
|
+
const npmCliPath = await getNpmCliPath();
|
|
24
|
+
return Process.spawn('node', [npmCliPath, ...arguments_], {
|
|
25
|
+
detached: true
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const getRoots = memoize(async () => {
|
|
29
|
+
const localRootProcess = await npm('root');
|
|
30
|
+
const globalRootProcess = await npm('root', '--global');
|
|
31
|
+
await Promise.all([localRootProcess.closed, globalRootProcess.closed]);
|
|
32
|
+
const local = localRootProcess.stdout.trim();
|
|
33
|
+
const global = globalRootProcess.stdout.trim();
|
|
34
|
+
logger.debug({ source: 'npm', message: 'Roots', data: { local, global } });
|
|
35
|
+
return {
|
|
36
|
+
local: localRootProcess.stdout.trim(),
|
|
37
|
+
global: globalRootProcess.stdout.trim()
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
export const Npm = {
|
|
41
|
+
async getLatestVersion(moduleName) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await Http.get(`https://registry.npmjs.org/${moduleName}/latest`);
|
|
44
|
+
const { version } = JSON.parse(response);
|
|
45
|
+
return version;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`Unable to fetch latest version of ${moduleName} from NPM registry`, {
|
|
49
|
+
cause: error
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
async checkIfLatestVersion(moduleName, isLocal) {
|
|
54
|
+
try {
|
|
55
|
+
const { local, global } = await getRoots();
|
|
56
|
+
const { version: installedVersion } = JSON.parse(await FileSystem.readFile(Path.join(isLocal ? local : global, moduleName, 'package.json'), 'utf8'));
|
|
57
|
+
logger.info({ source: 'npm', message: `Installed version of ${moduleName} is ${installedVersion}` });
|
|
58
|
+
const latestVersion = await Npm.getLatestVersion(moduleName);
|
|
59
|
+
if (latestVersion !== installedVersion) {
|
|
60
|
+
logger.warn({ source: 'npm', message: `[PKGVRS] Latest version of ${moduleName} is ${latestVersion}` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.error({ source: 'npm', message: 'Failed in checkIfLatestVersion', error });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async import(moduleName) {
|
|
68
|
+
logger.debug({ source: 'npm', message: `Npm.import(${moduleName})` });
|
|
69
|
+
try {
|
|
70
|
+
const module = (await import(moduleName));
|
|
71
|
+
logger.debug({ source: 'npm', message: `Module ${moduleName} found locally` });
|
|
72
|
+
void this.checkIfLatestVersion(moduleName, true);
|
|
73
|
+
return module;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
logger.warn({ source: 'npm', message: `Module ${moduleName} not found locally` });
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Not implemented');
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { logger, Exit, Process } from '../platform/index.js';
|
|
2
|
+
import { Npm } from '../Npm.js';
|
|
3
|
+
import { agentLogPrefix } from '../types/AgentState.js';
|
|
4
|
+
export const factory = async () => {
|
|
5
|
+
const puppeteer = await Npm.import('puppeteer');
|
|
6
|
+
const { launch } = puppeteer;
|
|
7
|
+
let browser;
|
|
8
|
+
const abortController = new AbortController();
|
|
9
|
+
const { signal } = abortController;
|
|
10
|
+
const task = Exit.registerAsyncTask({
|
|
11
|
+
name: 'puppeteer',
|
|
12
|
+
async stop() {
|
|
13
|
+
abortController.abort();
|
|
14
|
+
await browser?.close();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
let openedPages = 0;
|
|
18
|
+
const launchAndInstallIfNeeded = async (settings) => {
|
|
19
|
+
const launchOptions = {
|
|
20
|
+
headless: !settings.visible,
|
|
21
|
+
defaultViewport: null,
|
|
22
|
+
handleSIGINT: false,
|
|
23
|
+
signal,
|
|
24
|
+
args: ['--start-maximized']
|
|
25
|
+
};
|
|
26
|
+
try {
|
|
27
|
+
browser = await launch(launchOptions);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof Error && error.message.startsWith('Could not find Chrome')) {
|
|
31
|
+
logger.info({
|
|
32
|
+
source: 'progress',
|
|
33
|
+
message: 'Installing chrome (puppeteer)',
|
|
34
|
+
pageId: undefined,
|
|
35
|
+
data: {
|
|
36
|
+
value: 1,
|
|
37
|
+
max: 0
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
await Process.spawn('npx', 'puppeteer browsers install chrome'.split(' '), {
|
|
41
|
+
shell: true,
|
|
42
|
+
signal
|
|
43
|
+
}).closed;
|
|
44
|
+
browser = await launch(launchOptions);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
logger.debug({ source: 'puppeteer', message: 'setup completed' });
|
|
51
|
+
return {
|
|
52
|
+
screenshotFormat: '.png'
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
async setup(settings) {
|
|
57
|
+
logger.debug({ source: 'puppeteer', message: 'setup', data: settings });
|
|
58
|
+
try {
|
|
59
|
+
return await launchAndInstallIfNeeded(settings);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.error({ source: 'puppeteer', message: 'setup failed', error });
|
|
63
|
+
task[Symbol.dispose]();
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async newWindow(settings) {
|
|
68
|
+
logger.debug({ source: 'puppeteer', message: 'newWindow', data: settings });
|
|
69
|
+
let page;
|
|
70
|
+
if (++openedPages === 1) {
|
|
71
|
+
const pages = await browser?.pages(true);
|
|
72
|
+
page = pages?.[0];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
page = await browser?.newPage({
|
|
76
|
+
type: 'window'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const script of settings.scripts) {
|
|
80
|
+
await page?.evaluateOnNewDocument(script);
|
|
81
|
+
}
|
|
82
|
+
const { pageId } = settings;
|
|
83
|
+
page
|
|
84
|
+
?.on('console', (message) => {
|
|
85
|
+
const LOG_TYPES = {
|
|
86
|
+
error: 'error',
|
|
87
|
+
warn: 'warn',
|
|
88
|
+
debug: 'debug'
|
|
89
|
+
};
|
|
90
|
+
const logType = LOG_TYPES[message.type()] ?? 'info';
|
|
91
|
+
let source;
|
|
92
|
+
let messageText = message.text();
|
|
93
|
+
if (messageText.startsWith(agentLogPrefix)) {
|
|
94
|
+
source = 'browser/agent';
|
|
95
|
+
messageText = messageText.slice(agentLogPrefix.length);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
source = 'browser/console';
|
|
99
|
+
}
|
|
100
|
+
logger[logType]({
|
|
101
|
+
source,
|
|
102
|
+
message: messageText,
|
|
103
|
+
pageId,
|
|
104
|
+
data: { type: message.type() }
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
?.on('response', (response) => {
|
|
108
|
+
const request = response.request();
|
|
109
|
+
const statusType = Math.floor(response.status() / 100);
|
|
110
|
+
const LOG_TYPES = [null, null, null, null, 'warn', 'error'];
|
|
111
|
+
const logType = LOG_TYPES[statusType] ?? 'info';
|
|
112
|
+
logger[logType]({
|
|
113
|
+
source: 'browser/network',
|
|
114
|
+
message: request.url(),
|
|
115
|
+
pageId,
|
|
116
|
+
data: {
|
|
117
|
+
request: {
|
|
118
|
+
method: request.method(),
|
|
119
|
+
headers: request.headers()
|
|
120
|
+
},
|
|
121
|
+
response: {
|
|
122
|
+
status: response.status(),
|
|
123
|
+
headers: response.headers()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
await page?.goto(settings.url);
|
|
129
|
+
logger.debug({ source: 'puppeteer', message: 'newWindow completed', data: settings });
|
|
130
|
+
return {
|
|
131
|
+
async eval(script) {
|
|
132
|
+
return await page?.evaluate(script);
|
|
133
|
+
},
|
|
134
|
+
screenshot() {
|
|
135
|
+
throw new Error('Not implemented');
|
|
136
|
+
},
|
|
137
|
+
async close() {
|
|
138
|
+
try {
|
|
139
|
+
await page?.close();
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
logger.error({ source: 'puppeteer', message: 'page.close failed', error });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
async shutdown() {
|
|
148
|
+
logger.debug({ source: 'puppeteer', message: 'shutdown' });
|
|
149
|
+
try {
|
|
150
|
+
await browser?.close();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.error({ source: 'puppeteer', message: 'browser.close failed', error });
|
|
154
|
+
}
|
|
155
|
+
task[Symbol.dispose]();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Exit, Host } from './platform/index.js';
|
|
3
|
+
import { CommandLine } from './configuration/CommandLine.js';
|
|
4
|
+
import { execute } from './modes/execute.js';
|
|
5
|
+
const main = async () => {
|
|
6
|
+
const indexOfCli = process.argv.findIndex((value) => /[\\/]cli(\.[tj]s)?$/.exec(value));
|
|
7
|
+
const configuration = await CommandLine.buildConfigurationFrom(Host.cwd(), process.argv.slice(indexOfCli + 1));
|
|
8
|
+
await execute(configuration);
|
|
9
|
+
};
|
|
10
|
+
main()
|
|
11
|
+
.catch((error) => {
|
|
12
|
+
console.error(error);
|
|
13
|
+
Exit.code = -1;
|
|
14
|
+
})
|
|
15
|
+
.finally(async () => {
|
|
16
|
+
await Exit.shutdown();
|
|
17
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { indexedOptions } from './indexedOptions.js';
|
|
2
|
+
import { ConfigurationValidator } from './ConfigurationValidator.js';
|
|
3
|
+
import { OptionValidationError } from './OptionValidationError.js';
|
|
4
|
+
import { looksLikeAnUrl } from './validators/url.js';
|
|
5
|
+
const setOption = (configuration, option, value) => {
|
|
6
|
+
const name = option.name;
|
|
7
|
+
if (value === undefined) {
|
|
8
|
+
if (option.type === 'boolean') {
|
|
9
|
+
Object.assign(configuration, {
|
|
10
|
+
[name]: true
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
else if (option.multiple !== true) {
|
|
14
|
+
configuration.errors.push(new OptionValidationError(option, 'Missing value'));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
if (option.multiple) {
|
|
19
|
+
if (!(option.name in configuration)) {
|
|
20
|
+
Object.assign(configuration, {
|
|
21
|
+
[name]: []
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
configuration[name].push(value);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
Object.assign(configuration, {
|
|
28
|
+
[name]: value
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const switchOption = (configuration, currentOption, name) => {
|
|
34
|
+
if (currentOption) {
|
|
35
|
+
setOption(configuration, currentOption);
|
|
36
|
+
}
|
|
37
|
+
const option = indexedOptions[name];
|
|
38
|
+
if (!option) {
|
|
39
|
+
throw OptionValidationError.createUnknown(name);
|
|
40
|
+
}
|
|
41
|
+
return option;
|
|
42
|
+
};
|
|
43
|
+
const positionalOption = {
|
|
44
|
+
name: 'positional',
|
|
45
|
+
description: 'Any argument not prefixed with an option',
|
|
46
|
+
type: 'string'
|
|
47
|
+
};
|
|
48
|
+
const handlePositional = (configuration, value) => {
|
|
49
|
+
if (looksLikeAnUrl(value)) {
|
|
50
|
+
setOption(configuration, indexedOptions.url, value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const shortcuts = ['capabilities', 'version', 'help'];
|
|
54
|
+
for (const shortcut of shortcuts) {
|
|
55
|
+
if (value === shortcut) {
|
|
56
|
+
setOption(configuration, indexedOptions[shortcut]);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
configuration.errors.push(new OptionValidationError(positionalOption, `Unable to process: ${value}`));
|
|
61
|
+
};
|
|
62
|
+
const traverseArguments = (configuration, argv) => {
|
|
63
|
+
let currentOption;
|
|
64
|
+
for (const argument of argv) {
|
|
65
|
+
if (argument.startsWith('--')) {
|
|
66
|
+
currentOption = switchOption(configuration, currentOption, argument.slice(2));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (argument.startsWith('-')) {
|
|
70
|
+
currentOption = switchOption(configuration, currentOption, argument.slice(1));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (currentOption === undefined) {
|
|
74
|
+
handlePositional(configuration, argument);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
setOption(configuration, currentOption, argument);
|
|
78
|
+
if (currentOption.multiple !== true) {
|
|
79
|
+
currentOption = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (currentOption) {
|
|
84
|
+
setOption(configuration, currentOption);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
export const CommandLine = {
|
|
88
|
+
async buildConfigurationFrom(cwd, argv) {
|
|
89
|
+
const configuration = { cwd, errors: [] };
|
|
90
|
+
traverseArguments(configuration, argv);
|
|
91
|
+
const { errors, ...configWithoutErrors } = configuration;
|
|
92
|
+
let validatedConfiguration;
|
|
93
|
+
try {
|
|
94
|
+
validatedConfiguration = await ConfigurationValidator.validate(configWithoutErrors);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (errors.length > 0 && error instanceof AggregateError) {
|
|
98
|
+
errors.push(...error.errors);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
errors.push(error);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (errors.length === 1) {
|
|
105
|
+
throw errors[0];
|
|
106
|
+
}
|
|
107
|
+
else if (errors.length > 0) {
|
|
108
|
+
throw new AggregateError(errors, 'Multiple errors occurred');
|
|
109
|
+
}
|
|
110
|
+
return validatedConfiguration;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { options, defaults } from './options.js';
|
|
2
|
+
import { indexedOptions } from './indexedOptions.js';
|
|
3
|
+
import { validators } from './validators/index.js';
|
|
4
|
+
import { OptionValidationError } from './OptionValidationError.js';
|
|
5
|
+
import { Modes } from '../modes/Modes.js';
|
|
6
|
+
const assertIfConfiguration = (value) => {
|
|
7
|
+
const errors = [];
|
|
8
|
+
for (const key of Object.keys(value)) {
|
|
9
|
+
const option = indexedOptions[key];
|
|
10
|
+
if (option) {
|
|
11
|
+
if (key === option.short) {
|
|
12
|
+
errors.push(new OptionValidationError(option, 'Do not use short name'));
|
|
13
|
+
}
|
|
14
|
+
else if (key !== option.name) {
|
|
15
|
+
errors.push(new OptionValidationError(option, 'Do not use kebab-case'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
errors.push(OptionValidationError.createUnknown(key));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (errors.length > 1) {
|
|
23
|
+
throw new AggregateError(errors, 'Unknown keys');
|
|
24
|
+
}
|
|
25
|
+
const [error] = errors;
|
|
26
|
+
if (error) {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const validateValue = async (option, configuration) => {
|
|
31
|
+
const value = configuration[option.name];
|
|
32
|
+
if ('multiple' in option) {
|
|
33
|
+
const validatedValues = [];
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
for (const valueItem of value) {
|
|
36
|
+
validatedValues.push(await validators[option.type](option, valueItem, configuration));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
validatedValues.push(await validators[option.type](option, value, configuration));
|
|
41
|
+
}
|
|
42
|
+
return validatedValues;
|
|
43
|
+
}
|
|
44
|
+
return await validators[option.type](option, value, configuration);
|
|
45
|
+
};
|
|
46
|
+
export const ConfigurationValidator = {
|
|
47
|
+
merge(configuration) {
|
|
48
|
+
return Promise.resolve(configuration);
|
|
49
|
+
},
|
|
50
|
+
computeMode(configuration) {
|
|
51
|
+
if (configuration.help) {
|
|
52
|
+
return Modes.help;
|
|
53
|
+
}
|
|
54
|
+
if (configuration.version) {
|
|
55
|
+
return Modes.version;
|
|
56
|
+
}
|
|
57
|
+
if (configuration.log) {
|
|
58
|
+
return Modes.log;
|
|
59
|
+
}
|
|
60
|
+
if (configuration.url) {
|
|
61
|
+
return Modes.remote;
|
|
62
|
+
}
|
|
63
|
+
return Modes.legacy;
|
|
64
|
+
},
|
|
65
|
+
async validate(configuration) {
|
|
66
|
+
const withDefaults = Object.assign(Object.create(defaults), configuration);
|
|
67
|
+
assertIfConfiguration(withDefaults);
|
|
68
|
+
const merged = await this.merge(withDefaults);
|
|
69
|
+
merged.mode = this.computeMode(merged);
|
|
70
|
+
for (const option of options) {
|
|
71
|
+
if (Object.hasOwnProperty.call(merged, option.name) || (merged[option.name] && option.type === 'fs-entry')) {
|
|
72
|
+
Object.assign(merged, {
|
|
73
|
+
[option.name]: await validateValue(option, merged)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return merged;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class OptionValidationError extends Error {
|
|
2
|
+
static createUnknown(name) {
|
|
3
|
+
return new OptionValidationError({ name, type: 'string', description: 'unknown' }, 'Unknown option');
|
|
4
|
+
}
|
|
5
|
+
_option;
|
|
6
|
+
constructor(option, message = 'Invalid value', cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this._option = option;
|
|
9
|
+
this.name = 'OptionValidationError';
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
get option() {
|
|
13
|
+
return this._option;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { options } from './options.js';
|
|
2
|
+
export const indexedOptions = {};
|
|
3
|
+
for (const option of options) {
|
|
4
|
+
const { name } = option;
|
|
5
|
+
indexedOptions[name] = option;
|
|
6
|
+
if ('short' in option) {
|
|
7
|
+
indexedOptions[option.short] = option;
|
|
8
|
+
}
|
|
9
|
+
const kebabCase = name.replaceAll(/[A-Z]/g, (letter) => `-${letter.toLocaleLowerCase()}`);
|
|
10
|
+
if (name !== kebabCase) {
|
|
11
|
+
indexedOptions[kebabCase] = option;
|
|
12
|
+
}
|
|
13
|
+
}
|