ui5-test-runner 6.0.0-beta.5 → 6.0.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 (130) hide show
  1. package/README.md +20 -29
  2. package/dist/Command.js +52 -0
  3. package/dist/Npm.js +154 -74
  4. package/dist/browsers/RecoverableBrowserError.js +6 -0
  5. package/dist/browsers/chromeArguments.js +5 -0
  6. package/dist/browsers/chromeOptions.js +29 -0
  7. package/dist/browsers/consoleMessage.js +23 -0
  8. package/dist/browsers/factory.js +168 -3
  9. package/dist/browsers/networkResponse.js +15 -0
  10. package/dist/browsers/playwright.js +104 -0
  11. package/dist/browsers/puppeteer.js +54 -88
  12. package/dist/browsers/seleniumWebdriver.js +124 -0
  13. package/dist/browsers/webdriverio.js +85 -0
  14. package/dist/cli.js +18 -12
  15. package/dist/configuration/CommandLine.js +46 -26
  16. package/dist/configuration/ConfigurationValidator.js +132 -18
  17. package/dist/configuration/OptionValidationError.js +50 -2
  18. package/dist/configuration/derivations.js +8 -0
  19. package/dist/configuration/options.js +380 -23
  20. package/dist/configuration/validations.js +135 -0
  21. package/dist/configuration/validators/boolean.js +1 -1
  22. package/dist/configuration/validators/enumeration.js +7 -0
  23. package/dist/configuration/validators/fsEntry.js +12 -12
  24. package/dist/configuration/validators/index.js +8 -2
  25. package/dist/configuration/validators/integer.js +7 -1
  26. package/dist/configuration/validators/json.js +23 -0
  27. package/dist/configuration/validators/libraryMapping.js +28 -0
  28. package/dist/configuration/validators/percent.js +2 -2
  29. package/dist/configuration/validators/regexp.js +2 -2
  30. package/dist/configuration/validators/secret.js +8 -0
  31. package/dist/configuration/validators/string.js +1 -1
  32. package/dist/configuration/validators/timeout.js +2 -2
  33. package/dist/configuration/validators/url.js +3 -3
  34. package/dist/end.js +58 -0
  35. package/dist/if.js +22 -0
  36. package/dist/modes/Modes.js +2 -1
  37. package/dist/modes/batch/BatchItem.js +1 -0
  38. package/dist/modes/batch/batchTask.js +159 -0
  39. package/dist/modes/batch/index.js +98 -0
  40. package/dist/modes/batch/output.js +22 -0
  41. package/dist/modes/batch/report.js +45 -0
  42. package/dist/modes/batch/resolve.js +105 -0
  43. package/dist/modes/dumpConfig.js +6 -0
  44. package/dist/modes/execute.js +6 -5
  45. package/dist/modes/help.js +87 -2
  46. package/dist/modes/log/LogReader.js +3 -2
  47. package/dist/modes/log/LogStorage.js +31 -27
  48. package/dist/modes/log/index.js +38 -6
  49. package/dist/modes/log/{REserve.js → reserve.js} +25 -2
  50. package/dist/modes/mcp/IDocument.js +2 -0
  51. package/dist/modes/mcp/frontmatter.js +142 -0
  52. package/dist/modes/mcp/index.js +49 -0
  53. package/dist/modes/mcp/knowledgeBase.js +104 -0
  54. package/dist/modes/mcp/remoteKnowledgeBase.js +79 -0
  55. package/dist/modes/mcp/reserve.js +175 -0
  56. package/dist/modes/mcp/tools/getTopic.js +46 -0
  57. package/dist/modes/mcp/tools/index.js +6 -0
  58. package/dist/modes/mcp/tools/listTopics.js +14 -0
  59. package/dist/modes/test/PageContext.js +1 -0
  60. package/dist/modes/test/Server.js +99 -0
  61. package/dist/modes/test/browser.js +31 -17
  62. package/dist/modes/test/browserConfig.js +17 -0
  63. package/dist/modes/test/coverage/collect.js +38 -0
  64. package/dist/modes/test/coverage/index.js +3 -0
  65. package/dist/modes/test/coverage/instrument.js +70 -0
  66. package/dist/modes/test/coverage/nyc.js +15 -0
  67. package/dist/modes/test/coverage/report.js +71 -0
  68. package/dist/modes/test/coverage/settings.js +53 -0
  69. package/dist/modes/test/index.js +94 -34
  70. package/dist/modes/test/pageTask.js +293 -98
  71. package/dist/modes/test/report.js +20 -3
  72. package/dist/modes/test/reserve.js +76 -0
  73. package/dist/modes/test/screenshot.js +49 -0
  74. package/dist/platform/Crypto.js +9 -0
  75. package/dist/platform/Exit.js +33 -24
  76. package/dist/platform/FileSystem.js +6 -2
  77. package/dist/platform/Host.js +9 -1
  78. package/dist/platform/Http.js +54 -29
  79. package/dist/platform/Module.js +2 -1
  80. package/dist/platform/Path.js +5 -1
  81. package/dist/platform/Process.js +67 -13
  82. package/dist/platform/Terminal.js +12 -8
  83. package/dist/platform/Thread.js +1 -1
  84. package/dist/platform/assert.js +13 -12
  85. package/dist/platform/environment.js +9 -18
  86. package/dist/platform/index.js +1 -0
  87. package/dist/platform/logger/CompressionContext.js +117 -0
  88. package/dist/platform/logger/allCompressed.js +8 -7
  89. package/dist/platform/logger/compress.js +29 -129
  90. package/dist/platform/logger/output/BaseLoggerOutput.js +49 -31
  91. package/dist/platform/logger/output/InteractiveLoggerOutput.js +1 -1
  92. package/dist/platform/logger/output/LlmLoggerOutput.js +29 -0
  93. package/dist/platform/logger/output/factory.js +5 -1
  94. package/dist/platform/logger/output.js +24 -6
  95. package/dist/platform/logger/toInternalLogAttributes.js +10 -11
  96. package/dist/platform/logger/traceFileName.js +1 -0
  97. package/dist/platform/logger.js +52 -21
  98. package/dist/platform/mock.js +110 -19
  99. package/dist/platform/version.js +1 -2
  100. package/dist/platform/workerBootstrap.js +1 -1
  101. package/dist/reports/initReportBuilder.js +18 -0
  102. package/dist/reports/junit.js +64 -0
  103. package/dist/reports/saveReport.js +10 -0
  104. package/dist/reports/textReport.js +92 -0
  105. package/dist/reserveLogger.js +22 -0
  106. package/dist/sendToParentProcess.js +6 -0
  107. package/dist/start.js +79 -0
  108. package/dist/types/ISecretValue.js +1 -0
  109. package/dist/types/SecretValue.js +15 -0
  110. package/dist/ui/agent.js +4 -2
  111. package/dist/ui/html-report.js +2 -2
  112. package/dist/ui/lib.js +1 -1
  113. package/dist/ui/log-viewer.js +1 -1
  114. package/dist/utils/node/Folder.js +2 -4
  115. package/dist/utils/node/FramedStreamReader.js +4 -5
  116. package/dist/utils/node/FramedStreamWriter.js +6 -5
  117. package/dist/utils/node/anonymize.js +11 -0
  118. package/dist/utils/node/timeout.js +11 -0
  119. package/dist/utils/shared/ProgressBar.js +1 -2
  120. package/dist/utils/shared/TestReportBuilder.js +11 -7
  121. package/dist/utils/shared/host.js +11 -0
  122. package/dist/utils/shared/object.js +12 -3
  123. package/dist/utils/shared/schema.js +20 -0
  124. package/dist/utils/shared/string.js +1 -0
  125. package/dist/utils/shared/toIError.js +3 -7
  126. package/dist/utils/shared/version.js +52 -0
  127. package/package.json +3 -79
  128. package/dist/configuration/validators/browser.js +0 -11
  129. package/dist/modes/test/REserve.js +0 -31
  130. package/dist/modes/test/server.js +0 -109
@@ -0,0 +1,45 @@
1
+ import { initReportBuilder } from '../../reports/initReportBuilder.js';
2
+ import { anonymize } from '../../utils/node/anonymize.js';
3
+ const itemStatus = (item) => {
4
+ if (item.skipped) {
5
+ return 'skipped';
6
+ }
7
+ return item.timedOut || item.statusCode !== 0 ? 'failed' : 'passed';
8
+ };
9
+ export const buildBatchReport = async (items, configuration) => {
10
+ let start = Date.now();
11
+ for (const item of items) {
12
+ start = Math.min(item.start?.getTime() ?? start, start);
13
+ }
14
+ let stop = start;
15
+ for (const item of items) {
16
+ stop = Math.max(item.end?.getTime() ?? stop, stop);
17
+ }
18
+ const tests = items.map((item) => {
19
+ const itemStart = item.start?.getTime() ?? start;
20
+ const itemStop = item.end?.getTime() ?? stop;
21
+ const status = itemStatus(item);
22
+ return {
23
+ name: item.label,
24
+ status,
25
+ duration: itemStop - itemStart,
26
+ start: itemStart,
27
+ stop: itemStop,
28
+ filePath: anonymize(item).path,
29
+ ...(item.message !== undefined && { message: item.message })
30
+ };
31
+ });
32
+ const summary = {
33
+ tests: tests.length,
34
+ passed: tests.filter((test) => test.status === 'passed').length,
35
+ failed: tests.filter((test) => test.status === 'failed').length,
36
+ skipped: tests.filter((test) => test.status === 'skipped').length,
37
+ pending: 0,
38
+ other: 0,
39
+ start,
40
+ stop,
41
+ duration: stop - start
42
+ };
43
+ const builder = await initReportBuilder(configuration);
44
+ return { ...builder.report, results: { ...builder.report.results, summary, tests } };
45
+ };
@@ -0,0 +1,105 @@
1
+ import { FileSystem, Path, logger } from '../../platform/index.js';
2
+ const folder = (items, folderPath) => {
3
+ const id = Path.basename(folderPath);
4
+ items.push({
5
+ path: folderPath,
6
+ id,
7
+ label: id,
8
+ args: ['--cwd', folderPath]
9
+ });
10
+ };
11
+ const configurationFile = async (items, configPath) => {
12
+ try {
13
+ const content = await FileSystem.readFile(configPath, 'utf8');
14
+ const parsed = JSON.parse(content);
15
+ const id = parsed.batchId ?? Path.basename(configPath);
16
+ const label = parsed.batchLabel ?? Path.basename(configPath);
17
+ items.push({
18
+ path: configPath,
19
+ id,
20
+ label,
21
+ args: ['--config', configPath]
22
+ });
23
+ }
24
+ catch {
25
+ logger.warn({
26
+ source: 'job',
27
+ message: 'batch item failed',
28
+ data: { path: configPath, reason: 'invalid JSON configuration file' }
29
+ });
30
+ }
31
+ };
32
+ const scan = async (items, cwd, re, root = cwd) => {
33
+ const names = await FileSystem.readdir(cwd);
34
+ let count = 0;
35
+ for (const name of names) {
36
+ const path = Path.join(cwd, name);
37
+ if (name === 'node_modules' || name.startsWith('.')) {
38
+ logger.debug({
39
+ source: 'job',
40
+ message: `Ignored ${path} while scanning for batch items`
41
+ });
42
+ continue;
43
+ }
44
+ const relativePath = path.slice(root.length + 1).replaceAll('\\', '/');
45
+ const pathStat = await FileSystem.stat(path);
46
+ if (pathStat.isDirectory()) {
47
+ if (re.test(relativePath)) {
48
+ folder(items, path);
49
+ continue;
50
+ }
51
+ count += await scan(items, path, re, root);
52
+ }
53
+ else if (pathStat.isFile()) {
54
+ ++count;
55
+ if (re.test(relativePath)) {
56
+ await configurationFile(items, path);
57
+ }
58
+ }
59
+ }
60
+ return count;
61
+ };
62
+ export const resolve = async (configuration) => {
63
+ const items = [];
64
+ const specs = configuration.batch ?? [];
65
+ for (const spec of specs) {
66
+ try {
67
+ const path = Path.isAbsolute(spec) ? spec : Path.join(configuration.cwd, spec);
68
+ const pathStat = await FileSystem.stat(path);
69
+ if (pathStat.isDirectory()) {
70
+ folder(items, path);
71
+ }
72
+ else if (pathStat.isFile() && path.endsWith('.json')) {
73
+ await configurationFile(items, path);
74
+ }
75
+ else {
76
+ logger.warn({
77
+ source: 'job',
78
+ message: 'batch item failed',
79
+ data: { path: spec, reason: 'only folders and JSON configuration files are supported' }
80
+ });
81
+ }
82
+ continue;
83
+ }
84
+ catch {
85
+ }
86
+ let re;
87
+ try {
88
+ re = new RegExp(spec);
89
+ }
90
+ catch {
91
+ logger.warn({
92
+ source: 'job',
93
+ message: 'batch item failed',
94
+ data: { path: spec, reason: 'invalid regular expression' }
95
+ });
96
+ continue;
97
+ }
98
+ const scanned = await scan(items, configuration.cwd, re);
99
+ logger.debug({
100
+ source: 'job',
101
+ message: `Scanned ${scanned} items`
102
+ });
103
+ }
104
+ return items;
105
+ };
@@ -0,0 +1,6 @@
1
+ import { redactSecrets } from '../utils/node/anonymize.js';
2
+ import { toPlainObject } from '../utils/shared/object.js';
3
+ export const dumpConfig = (configuration) => {
4
+ const safe = redactSecrets(toPlainObject(configuration));
5
+ console.log(JSON.stringify(safe, null, 2));
6
+ };
@@ -1,18 +1,19 @@
1
+ import { dumpConfig } from './dumpConfig.js';
1
2
  import { help } from './help.js';
2
3
  import { log } from './log/index.js';
4
+ import { mcp } from './mcp/index.js';
3
5
  import { Modes } from './Modes.js';
4
6
  import { version } from './version.js';
5
7
  import { test } from './test/index.js';
8
+ import { batch } from './batch/index.js';
6
9
  import { Exit } from '../platform/Exit.js';
7
- const notImplemented = () => {
8
- throw new Error('Not implemented');
9
- };
10
10
  const modeFunctions = {
11
- [Modes.batch]: notImplemented,
12
- [Modes.capabilities]: notImplemented,
11
+ [Modes.batch]: batch,
12
+ [Modes.dumpConfig]: dumpConfig,
13
13
  [Modes.help]: help,
14
14
  [Modes.legacy]: test,
15
15
  [Modes.log]: log,
16
+ [Modes.mcp]: mcp,
16
17
  [Modes.remote]: test,
17
18
  [Modes.version]: version
18
19
  };
@@ -1,3 +1,88 @@
1
- export const help = () => {
2
- console.log('Please check https://arnaudbuchholz.github.io/ui5-test-runner/');
1
+ import { options } from '../configuration/options.js';
2
+ import { Terminal } from '../platform/index.js';
3
+ import { version } from '../platform/version.js';
4
+ import { Npm } from '../Npm.js';
5
+ import { toKebabCase } from '../utils/shared/string.js';
6
+ const FALLBACK_WIDTH = 80;
7
+ export const DOCUMENTATION_LINK = 'https://github.com/ArnaudBuchholz/ui5-test-runner/tree/main/docs#-documentation';
8
+ const formatDefault = (value) => {
9
+ return Array.isArray(value) ? value.join(', ') : String(value);
10
+ };
11
+ const wrapText = (text, width, indent) => {
12
+ if (width <= 0) {
13
+ return text;
14
+ }
15
+ const words = text.split(' ');
16
+ const lines = [];
17
+ let current = '';
18
+ for (const word of words) {
19
+ if (current.length === 0) {
20
+ current = word;
21
+ }
22
+ else if (current.length + 1 + word.length <= width) {
23
+ current += ` ${word}`;
24
+ }
25
+ else {
26
+ lines.push(current);
27
+ current = word;
28
+ }
29
+ }
30
+ if (current.length > 0) {
31
+ lines.push(current);
32
+ }
33
+ return lines.join(`\n${indent}`);
34
+ };
35
+ const optionFlags = (option) => {
36
+ const short = 'short' in option ? option.short : undefined;
37
+ const kebab = toKebabCase(option.name);
38
+ return short ? `--${kebab}, -${short}` : `--${kebab}`;
39
+ };
40
+ const TYPE_DISPLAY = {
41
+ 'fs-entry': 'path',
42
+ 'library-mapping': 'mapping'
43
+ };
44
+ const optionTypeLabel = (option) => {
45
+ const isMultiple = 'multiple' in option && option.multiple;
46
+ const display = TYPE_DISPLAY[option.type] ?? option.type;
47
+ return isMultiple ? `<${display}...>` : `<${display}>`;
48
+ };
49
+ const optionDescription = (option) => !('typeModifiers' in option) || !option.typeModifiers
50
+ ? option.description
51
+ : `${option.description} (${[...option.typeModifiers].join(', ')})`;
52
+ export const help = async () => {
53
+ const { name, version: semver } = await version();
54
+ console.log(`${name}@${semver}`);
55
+ let latestVersion;
56
+ try {
57
+ latestVersion = await Npm.getLatestVersion(name);
58
+ }
59
+ catch {
60
+ }
61
+ if (latestVersion !== undefined && latestVersion !== semver) {
62
+ console.log(`Latest version of ${name} is ${latestVersion}`);
63
+ }
64
+ console.log('');
65
+ console.log('Usage: ui5-test-runner [options]\n');
66
+ console.log('Options:\n');
67
+ const col1Width = Math.max(...options.map((option) => optionFlags(option).length));
68
+ const col2Width = Math.max(...options.map((option) => optionTypeLabel(option).length));
69
+ const terminalWidth = Terminal.width || FALLBACK_WIDTH;
70
+ const descIndentWidth = 2 + col1Width + 2 + col2Width + 2;
71
+ const descWidth = terminalWidth - descIndentWidth;
72
+ const descIndent = ' '.repeat(descIndentWidth);
73
+ const getDefaultLabel = (option) => {
74
+ if ('defaultLabel' in option) {
75
+ return option.defaultLabel;
76
+ }
77
+ return 'default' in option ? formatDefault(option.default) : undefined;
78
+ };
79
+ for (const option of options) {
80
+ const flags = optionFlags(option);
81
+ const typeLabel = optionTypeLabel(option);
82
+ const defaultLabel = getDefaultLabel(option);
83
+ const defaultPart = defaultLabel === undefined ? '' : ` [default: ${defaultLabel}]`;
84
+ const desc = wrapText(`${optionDescription(option)}${defaultPart}`, descWidth, descIndent);
85
+ console.log(` ${flags.padEnd(col1Width)} ${typeLabel.padEnd(col2Width)} ${desc}`);
86
+ }
87
+ console.log(`\nFor full documentation, visit ${DOCUMENTATION_LINK}`);
3
88
  };
@@ -1,11 +1,12 @@
1
1
  import { ZLib } from '../../platform/index.js';
2
- import { createCompressionContext, uncompress } from '../../platform/logger/compress.js';
2
+ import { CompressionContext } from '../../platform/logger/CompressionContext.js';
3
+ import { uncompress } from '../../platform/logger/compress.js';
3
4
  import { FramedStreamReader } from '../../utils/node/FramedStreamReader.js';
4
5
  import { getInitialLogMetrics } from './LogMetrics.js';
5
6
  export const POLL_INTERVAL_MS = 500;
6
7
  export const LogReader = {
7
8
  async *read(logFileName, signal) {
8
- const context = createCompressionContext();
9
+ const context = CompressionContext.create();
9
10
  const stream = FramedStreamReader.create(logFileName, POLL_INTERVAL_MS);
10
11
  const metrics = getInitialLogMetrics();
11
12
  for await (const chunk of stream.read(signal)) {
@@ -17,52 +17,56 @@ export class LogStorage {
17
17
  this._logsAdded = true;
18
18
  }
19
19
  _sortIfNeeded() {
20
- if (this._logsAdded) {
21
- this._logs.sort((a, b) => a.timestamp - b.timestamp);
22
- this._logsAdded = false;
20
+ if (!this._logsAdded) {
21
+ return;
23
22
  }
23
+ this._logs.sort((a, b) => a.timestamp - b.timestamp);
24
+ this._logsAdded = false;
24
25
  }
25
26
  static buildFilterExpression(filter) {
26
- if (!filter) {
27
- return () => true;
28
- }
29
27
  const expression = punyexpr(filter);
30
28
  return (log) => !!expression({
31
29
  ...log,
32
30
  level: LOG_LEVELS[log.level],
33
- [punyexpr.propertyOf]: (value, property) => {
34
- if (value === undefined) {
35
- return;
36
- }
37
- return value[property];
38
- }
31
+ [punyexpr.propertyOf]: (value, property) => value && value[property]
39
32
  });
40
33
  }
41
- fetch(query = {}) {
42
- this._sortIfNeeded();
43
- const { from = 0, to = Number.MAX_SAFE_INTEGER, filter = '' } = query;
44
- let { skip = 0, limit = MAX_LIMIT } = query;
45
- const filterExpression = LogStorage.buildFilterExpression(filter);
46
- limit = Math.max(0, Math.min(limit, MAX_LIMIT));
34
+ #fetch(query) {
35
+ const { from, to, filter, skip, cappedLimit, hasRange, hasFilter } = query;
36
+ const filterExpression = hasFilter ? LogStorage.buildFilterExpression(filter) : null;
37
+ let startIndex = 0;
38
+ if (hasRange) {
39
+ startIndex = this._logs.findIndex((log) => log.timestamp >= from);
40
+ if (startIndex === -1) {
41
+ return [];
42
+ }
43
+ }
47
44
  const records = [];
48
- for (let index = 0; limit > 0 && index < this._logs.length; ++index) {
45
+ let skipped = 0;
46
+ for (let index = startIndex; index < this._logs.length && records.length < cappedLimit; ++index) {
49
47
  const log = this._logs[index];
50
- if (log.timestamp < from) {
51
- continue;
52
- }
53
- if (log.timestamp > to) {
48
+ if (hasRange && log.timestamp > to) {
54
49
  break;
55
50
  }
56
- if (skip > 0) {
57
- --skip;
51
+ if (filterExpression && !filterExpression(log)) {
58
52
  continue;
59
53
  }
60
- if (!filterExpression(log)) {
54
+ if (skipped < skip) {
55
+ ++skipped;
61
56
  continue;
62
57
  }
63
58
  records.push(log);
64
- --limit;
65
59
  }
66
60
  return records;
67
61
  }
62
+ fetch(query = {}) {
63
+ this._sortIfNeeded();
64
+ const { from = 0, to = Number.MAX_SAFE_INTEGER, filter = '', skip = 0, limit = MAX_LIMIT } = query;
65
+ const cappedLimit = Math.min(limit, MAX_LIMIT);
66
+ const hasRange = from !== 0 || to !== Number.MAX_SAFE_INTEGER;
67
+ const hasFilter = filter !== '';
68
+ return !hasRange && !hasFilter
69
+ ? this._logs.slice(skip, skip + Math.max(cappedLimit, 0))
70
+ : this.#fetch({ from, to, filter, skip, limit, cappedLimit, hasRange, hasFilter });
71
+ }
68
72
  }
@@ -2,20 +2,51 @@ import { __developmentMode, Exit } from '../../platform/index.js';
2
2
  import { LogReader } from './LogReader.js';
3
3
  import { serve } from 'reserve';
4
4
  import { LogStorage } from './LogStorage.js';
5
- import { buildREserveConfiguration } from './REserve.js';
5
+ import { buildREserveConfiguration } from './reserve.js';
6
6
  import { BrowserFactory } from '../../browsers/factory.js';
7
7
  import { getInitialLogMetrics } from './LogMetrics.js';
8
+ const dumpLogToStdout = async (configuration) => {
9
+ const logFileName = configuration.log;
10
+ const logFilter = configuration.logFilter;
11
+ const filterExpression = logFilter && logFilter.trim() !== '' ? LogStorage.buildFilterExpression(logFilter) : undefined;
12
+ process.stdout.write('[\n');
13
+ let isFirst = true;
14
+ for await (const item of LogReader.read(logFileName)) {
15
+ if (item.type !== 'log') {
16
+ continue;
17
+ }
18
+ const { type: _, ...logAttributes } = item;
19
+ if (filterExpression && !filterExpression(logAttributes)) {
20
+ continue;
21
+ }
22
+ if (!isFirst) {
23
+ process.stdout.write(',\n');
24
+ }
25
+ process.stdout.write(' ' + JSON.stringify(logAttributes));
26
+ isFirst = false;
27
+ }
28
+ process.stdout.write(isFirst ? ']\n' : '\n]\n');
29
+ };
8
30
  export const log = async (configuration) => {
31
+ if (configuration.logDump) {
32
+ await dumpLogToStdout(configuration);
33
+ return;
34
+ }
9
35
  const logFileName = configuration.log;
10
- let stopped = false;
36
+ let isStopped = false;
11
37
  const metrics = getInitialLogMetrics();
12
38
  const storage = LogStorage.create();
13
39
  const { promise, resolve } = Promise.withResolvers();
14
40
  const abortController = new AbortController();
15
41
  const abortSignal = abortController.signal;
16
- const server = serve(buildREserveConfiguration(storage, metrics, abortController));
42
+ const server = serve(buildREserveConfiguration({
43
+ configuration,
44
+ storage,
45
+ metrics,
46
+ abortController
47
+ }));
17
48
  const stop = async () => {
18
- stopped = true;
49
+ isStopped = true;
19
50
  await server?.close();
20
51
  resolve();
21
52
  };
@@ -43,12 +74,13 @@ export const log = async (configuration) => {
43
74
  });
44
75
  });
45
76
  for await (const item of LogReader.read(logFileName, abortSignal)) {
46
- if (stopped) {
77
+ if (isStopped) {
47
78
  break;
48
79
  }
49
80
  const { type, ...attributes } = item;
50
81
  if (type === 'log') {
51
- storage.add(attributes);
82
+ const { type: _, ...logAttributes } = item;
83
+ storage.add(logAttributes);
52
84
  }
53
85
  else if (__developmentMode) {
54
86
  Object.assign(metrics, attributes);
@@ -16,8 +16,8 @@ const getAsInt = (parameters, key) => {
16
16
  const value = parameters.get(key);
17
17
  return value ? Number.parseInt(value) : undefined;
18
18
  };
19
- export const buildREserveConfiguration = (storage, metrics, abortController) => ({
20
- port: 0,
19
+ export const buildREserveConfiguration = ({ configuration, storage, metrics, abortController }) => ({
20
+ port: configuration.port ?? 0,
21
21
  mappings: [
22
22
  {
23
23
  method: 'GET',
@@ -37,6 +37,29 @@ export const buildREserveConfiguration = (storage, metrics, abortController) =>
37
37
  ];
38
38
  }
39
39
  },
40
+ {
41
+ method: 'GET',
42
+ match: '/all',
43
+ custom: async (request, response) => {
44
+ response.writeHead(200, {
45
+ 'content-type': 'application/json'
46
+ });
47
+ response.write('[\n');
48
+ const { length } = storage;
49
+ for (let index = 0; index < length; ++index) {
50
+ const log = storage.fetch({
51
+ skip: index,
52
+ limit: 1
53
+ })[0];
54
+ const suffix = index < length - 1 ? ',\n' : '\n';
55
+ const isDrainNeeded = !response.write(' ' + JSON.stringify(log) + suffix);
56
+ if (isDrainNeeded) {
57
+ await new Promise((resolve) => response.once('drain', resolve));
58
+ }
59
+ }
60
+ response.end(']\n');
61
+ }
62
+ },
40
63
  {
41
64
  method: 'GET',
42
65
  match: '/lib.js',
@@ -0,0 +1,2 @@
1
+ const EXCLUDED_FOLDERS = ['adr', 'decks', 'prompts'];
2
+ export const isExcludedPath = (documentationRelativePath) => EXCLUDED_FOLDERS.some((folder) => documentationRelativePath.startsWith(`${folder}/`));
@@ -0,0 +1,142 @@
1
+ const FRONTMATTER_RE = /^---\n([\s\S]*?\n)---\n?([\s\S]*)$/;
2
+ const KNOWN_SCALARS = new Set(['#type', 'id', 'title', 'summary']);
3
+ const assignScalar = (frontmatter, key, value) => {
4
+ switch (key) {
5
+ case '#type': {
6
+ frontmatter['#type'] = value;
7
+ break;
8
+ }
9
+ case 'id': {
10
+ frontmatter.id = value;
11
+ break;
12
+ }
13
+ case 'title': {
14
+ frontmatter.title = value;
15
+ break;
16
+ }
17
+ case 'summary': {
18
+ frontmatter.summary = value;
19
+ break;
20
+ }
21
+ }
22
+ };
23
+ const stripQuotes = (value) => {
24
+ const trimmed = value.trim();
25
+ return trimmed.length >= 2 && (trimmed.startsWith('"') || trimmed.startsWith("'")) && trimmed.endsWith(trimmed[0])
26
+ ? trimmed.slice(1, -1)
27
+ : trimmed;
28
+ };
29
+ const parseInlineList = (value) => {
30
+ const trimmed = value.trim();
31
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
32
+ return null;
33
+ }
34
+ const inner = trimmed.slice(1, -1).trim();
35
+ return inner === '' ? [] : inner.split(',').map((item) => stripQuotes(item));
36
+ };
37
+ const measure = (line) => {
38
+ const trimmed = line.trimStart();
39
+ return { indent: line.length - trimmed.length, content: trimmed.trimEnd() };
40
+ };
41
+ const collectBlock = (lines, start, parentIndent) => {
42
+ const items = [];
43
+ let index = start;
44
+ while (index < lines.length) {
45
+ const { indent, content } = measure(lines[index]);
46
+ if (content === '') {
47
+ index += 1;
48
+ continue;
49
+ }
50
+ if (indent <= parentIndent || !content.startsWith('- ')) {
51
+ break;
52
+ }
53
+ items.push(stripQuotes(content.slice(2)));
54
+ index += 1;
55
+ }
56
+ return { items, next: index };
57
+ };
58
+ const readList = (lines, start, parentIndent, value) => {
59
+ if (value === '') {
60
+ const block = collectBlock(lines, start, parentIndent);
61
+ return { items: block.items, next: block.next };
62
+ }
63
+ return { items: parseInlineList(value), next: start };
64
+ };
65
+ const collectRelations = (lines, start, parentIndent) => {
66
+ const relations = {};
67
+ let index = start;
68
+ while (index < lines.length) {
69
+ const { indent, content } = measure(lines[index]);
70
+ if (content === '') {
71
+ index += 1;
72
+ continue;
73
+ }
74
+ if (indent <= parentIndent) {
75
+ break;
76
+ }
77
+ const colon = content.indexOf(':');
78
+ if (colon === -1) {
79
+ index += 1;
80
+ continue;
81
+ }
82
+ const key = content.slice(0, colon).trim();
83
+ const list = readList(lines, index + 1, indent, content.slice(colon + 1).trim());
84
+ index = list.next;
85
+ if (list.items !== null) {
86
+ relations[key] = list.items;
87
+ }
88
+ }
89
+ return { relations, next: index };
90
+ };
91
+ const skipIndentedBlock = (lines, start, parentIndent) => {
92
+ let index = start;
93
+ while (index < lines.length) {
94
+ const { indent, content } = measure(lines[index]);
95
+ if (content !== '' && indent <= parentIndent) {
96
+ break;
97
+ }
98
+ index += 1;
99
+ }
100
+ return index;
101
+ };
102
+ const applyEntry = (frontmatter, lines, index, indent, key, value) => {
103
+ if (key === 'relations' && value === '') {
104
+ const result = collectRelations(lines, index, indent);
105
+ frontmatter.relations = result.relations;
106
+ return result.next;
107
+ }
108
+ if (key === 'keywords') {
109
+ const list = readList(lines, index, indent, value);
110
+ if (list.items !== null) {
111
+ frontmatter.keywords = list.items;
112
+ }
113
+ return list.next;
114
+ }
115
+ if (value !== '' && KNOWN_SCALARS.has(key)) {
116
+ assignScalar(frontmatter, key, stripQuotes(value));
117
+ return index;
118
+ }
119
+ if (value === '') {
120
+ return skipIndentedBlock(lines, index, indent);
121
+ }
122
+ return index;
123
+ };
124
+ const parseMetadata = (raw) => {
125
+ const frontmatter = {};
126
+ const lines = raw.split('\n');
127
+ let index = 0;
128
+ while (index < lines.length) {
129
+ const { indent, content } = measure(lines[index]);
130
+ const colon = content.indexOf(':');
131
+ if (colon === -1 || indent > 0) {
132
+ index += 1;
133
+ continue;
134
+ }
135
+ index = applyEntry(frontmatter, lines, index + 1, indent, stripQuotes(content.slice(0, colon)), content.slice(colon + 1).trim());
136
+ }
137
+ return frontmatter;
138
+ };
139
+ export const parseFrontmatter = (content) => {
140
+ const match = FRONTMATTER_RE.exec(content);
141
+ return match ? { frontmatter: parseMetadata(match[1]), body: match[2] } : { frontmatter: null, body: content };
142
+ };