@orangebeard-io/playwright-orangebeard-reporter 1.0.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.
- package/.eslintignore +5 -0
- package/.eslintrc +22 -0
- package/.github/logo.svg +91 -0
- package/.github/workflows/release.yml +144 -0
- package/.prettierrc +7 -0
- package/LICENSE +661 -0
- package/README.md +90 -0
- package/changelog-template.hbs +29 -0
- package/dist/index.js +4 -0
- package/package.json +47 -0
- package/src/index.ts +3 -0
- package/src/reporter/OrangebeardReporter.ts +265 -0
- package/src/reporter/utils.ts +160 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
<h1 align="center">
|
2
|
+
<a href="https://github.com/orangebeard-io/playwright-listener">
|
3
|
+
<img src="https://raw.githubusercontent.com/orangebeard-io/playwright-listener/main/.github/logo.svg" alt="Orangebeard.io Playwright Listener" height="200">
|
4
|
+
</a>
|
5
|
+
<br>Orangebeard.io Playwright Listener<br>
|
6
|
+
</h1>
|
7
|
+
|
8
|
+
<h4 align="center">Orangebeard listener for <a href="https://playwright.dev" target="_blank" rel="noopener">Playwright</a></h4>
|
9
|
+
|
10
|
+
<p align="center">
|
11
|
+
<a href="https://www.npmjs.com/package/@orangebeard-io/playwright-orangebeard-reporter">
|
12
|
+
<img src="https://img.shields.io/npm/v/@orangebeard-io/playwright-orangebeard-reporter.svg?style=flat-square"
|
13
|
+
alt="NPM Version" />
|
14
|
+
</a>
|
15
|
+
<a href="https://github.com/orangebeard-io/playwright-listener/actions">
|
16
|
+
<img src="https://img.shields.io/github/actions/workflow/status/orangebeard-io/playwright-listener/release.yml?branch=main&style=flat-square"
|
17
|
+
alt="Build Status" />
|
18
|
+
</a>
|
19
|
+
<a href="https://github.com/orangebeard-io/playwright-listener/blob/main/LICENSE">
|
20
|
+
<img src="https://img.shields.io/github/license/orangebeard-io/playwright-listener?style=flat-square"
|
21
|
+
alt="License" />
|
22
|
+
</a>
|
23
|
+
</p>
|
24
|
+
|
25
|
+
<div align="center">
|
26
|
+
<h4>
|
27
|
+
<a href="https://orangebeard.io">Orangebeard</a> |
|
28
|
+
<a href="#installation">Installation</a> |
|
29
|
+
<a href="#configuration">Configuration</a>
|
30
|
+
</h4>
|
31
|
+
</div>
|
32
|
+
|
33
|
+
## Installation
|
34
|
+
|
35
|
+
### Install the npm package
|
36
|
+
|
37
|
+
```shell
|
38
|
+
npm install @orangebeard-io/playwright-orangebeard-reporter
|
39
|
+
```
|
40
|
+
|
41
|
+
## Configuration
|
42
|
+
|
43
|
+
Create orangebeard.json (in your test projects's folder (or above))
|
44
|
+
|
45
|
+
```JSON
|
46
|
+
{
|
47
|
+
"endpoint": "https://XXX.orangebeard.app",
|
48
|
+
"accessToken": "00000000-0000-0000-0000-00000000",
|
49
|
+
"project": "my_project_name",
|
50
|
+
"testset": "My Test Set Name",
|
51
|
+
"description": "A run from playwright",
|
52
|
+
"attributes": [
|
53
|
+
{
|
54
|
+
"key": "SomeKey",
|
55
|
+
"value": "SomeValue"
|
56
|
+
},
|
57
|
+
{
|
58
|
+
"value": "Tag value"
|
59
|
+
}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
```
|
63
|
+
|
64
|
+
Configure the reporter in playwright-config.ts:
|
65
|
+
```ts
|
66
|
+
export default defineConfig({
|
67
|
+
testDir: './my-tests',
|
68
|
+
reporter: [['@orangebeard-io/playwright-orangebeard-reporter']],
|
69
|
+
projects: [
|
70
|
+
{
|
71
|
+
name: 'chromium',
|
72
|
+
use: { ...devices['Desktop Chrome'] },
|
73
|
+
}]
|
74
|
+
});
|
75
|
+
```
|
76
|
+
|
77
|
+
### Running
|
78
|
+
|
79
|
+
Run your tests as usual!
|
80
|
+
|
81
|
+
Alternatively, configure Orangebeard variables as ENV (without or on top of orangebeard.json):
|
82
|
+
|
83
|
+
```shell
|
84
|
+
ORANGEBEARD_ENDPOINT=https://company.orangebeard.app
|
85
|
+
ORANGEBEARD_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
86
|
+
ORANGEBEARD_PROJECT="my project"
|
87
|
+
ORANGEBEARD_TESTSET="my test set"
|
88
|
+
ORANGEBEARD_DESCRIPTION="My awesome testrun"
|
89
|
+
ORANGEBEARD_ATTRIBUTES="key:value; value;"
|
90
|
+
```
|
@@ -0,0 +1,29 @@
|
|
1
|
+
{{#each releases}}
|
2
|
+
{{#if summary}}
|
3
|
+
{{summary}}
|
4
|
+
{{/if}}
|
5
|
+
|
6
|
+
{{#if merges}}
|
7
|
+
### :twisted_rightwards_arrows: Merged
|
8
|
+
|
9
|
+
{{#each merges}}
|
10
|
+
- {{#if commit.breaking}}**Breaking change:** {{/if}}{{message}} {{#if href}}[`#{{id}}`]({{href}}){{/if}}
|
11
|
+
{{/each}}
|
12
|
+
{{/if}}
|
13
|
+
|
14
|
+
{{#if fixes}}
|
15
|
+
### :bug: Fixed
|
16
|
+
|
17
|
+
{{#each fixes}}
|
18
|
+
- {{#if commit.breaking}}**Breaking change:** {{/if}}{{commit.subject}}{{#each fixes}} {{#if href}}[`#{{id}}`]({{href}}){{/if}}{{/each}}
|
19
|
+
{{/each}}
|
20
|
+
{{/if}}
|
21
|
+
|
22
|
+
{{#commit-list commits heading='### :mag: Commits'}}
|
23
|
+
- {{#if breaking}}**Breaking change:** {{/if}}{{subject}} {{#if href}}[`{{shorthash}}`]({{href}}){{/if}}
|
24
|
+
{{/commit-list}}
|
25
|
+
|
26
|
+
{{#if href}}
|
27
|
+
See full comparison at [{{title}}]({{href}})
|
28
|
+
{{/if}}
|
29
|
+
{{/each}}
|
package/dist/index.js
ADDED
package/package.json
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
{
|
2
|
+
"name": "@orangebeard-io/playwright-orangebeard-reporter",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "A Playwright reporter to report to Orangebeard.io",
|
5
|
+
"main": "dist/index.js",
|
6
|
+
"scripts": {
|
7
|
+
"build": "rimraf ./dist && tsc",
|
8
|
+
"lint": "eslint . --ext .ts",
|
9
|
+
"format:js": "npm run lint -- --fix",
|
10
|
+
"format:md": "prettier --write README.md",
|
11
|
+
"format": "npm run format:js && npm run format:md",
|
12
|
+
"test": "jest --unhandled-rejections=none --config ./jest.config.js",
|
13
|
+
"test:coverage": "jest --unhandled-rejections=none --coverage",
|
14
|
+
"get-version": "echo $npm_package_version",
|
15
|
+
"update-version": "release-it --ci --no-git --no-npm.publish",
|
16
|
+
"create-changelog": "auto-changelog --template changelog-template.hbs --starting-version v$npm_package_version"
|
17
|
+
},
|
18
|
+
"repository": {
|
19
|
+
"type": "git",
|
20
|
+
"url": "git+https://github.com/orangebeard-io/playwright-listener.git"
|
21
|
+
},
|
22
|
+
"keywords": [
|
23
|
+
"playwright",
|
24
|
+
"orangebeard",
|
25
|
+
"reporter",
|
26
|
+
"testing"
|
27
|
+
],
|
28
|
+
"author": "Orangebeard.io",
|
29
|
+
"license": "Apache-2.0",
|
30
|
+
"bugs": {
|
31
|
+
"url": "https://github.com/orangebeard-io/playwright-listener/issues"
|
32
|
+
},
|
33
|
+
"homepage": "https://github.com/orangebeard-io/playwright-listener#readme",
|
34
|
+
"devDependencies": {
|
35
|
+
"auto-changelog": "^2.5.0",
|
36
|
+
"@js-joda/core": "^5.6.3",
|
37
|
+
"@orangebeard-io/javascript-client": "^2.0.6",
|
38
|
+
"@playwright/test": "^1.49.0",
|
39
|
+
"@typescript-eslint/parser": "^8.15.0",
|
40
|
+
"eslint": "^9.15.0",
|
41
|
+
"eslint-config-prettier": "^9.1.0",
|
42
|
+
"eslint-plugin-prettier": "^5.2.1",
|
43
|
+
"prettier": "^3.3.3",
|
44
|
+
"rimraf": "^6.0.1",
|
45
|
+
"typescript": "^5.6.3"
|
46
|
+
}
|
47
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
import {UUID} from 'crypto';
|
2
|
+
import {Reporter, TestCase, TestResult, TestStep} from '@playwright/test/reporter'
|
3
|
+
import {ansiToMarkdown, getBytes, getCodeSnippet, getTime, removeAnsi, testStatusMap} from './utils'
|
4
|
+
import {OrangebeardParameters} from "@orangebeard-io/javascript-client/dist/client/models/OrangebeardParameters";
|
5
|
+
import OrangebeardAsyncV3Client from "@orangebeard-io/javascript-client/dist/client/OrangebeardAsyncV3Client";
|
6
|
+
import {StartTest} from "@orangebeard-io/javascript-client/dist/client/models/StartTest";
|
7
|
+
import {Attachment} from "@orangebeard-io/javascript-client/dist/client/models/Attachment";
|
8
|
+
import {Log} from "@orangebeard-io/javascript-client/dist/client/models/Log";
|
9
|
+
import {Attribute} from "@orangebeard-io/javascript-client/dist/client/models/Attribute";
|
10
|
+
import {FinishStep} from "@orangebeard-io/javascript-client/dist/client/models/FinishStep";
|
11
|
+
import TestType = StartTest.TestType;
|
12
|
+
import LogFormat = Log.LogFormat;
|
13
|
+
import LogLevel = Log.LogLevel;
|
14
|
+
import Status = FinishStep.Status;
|
15
|
+
import * as path from "node:path";
|
16
|
+
|
17
|
+
export class OrangebeardReporter implements Reporter {
|
18
|
+
|
19
|
+
config: OrangebeardParameters;
|
20
|
+
client: OrangebeardAsyncV3Client;
|
21
|
+
|
22
|
+
//CONTEXT TRACKING
|
23
|
+
testRunId: UUID;
|
24
|
+
suites: Map<string, UUID> = new Map<string, UUID>(); //suiteNames , uuid
|
25
|
+
tests: Map<string, UUID> = new Map<string, UUID>(); //testId, uuid
|
26
|
+
steps: Map<string, UUID> = new Map<string, UUID>(); //testId_stepPath, uuid
|
27
|
+
promises: Promise<void>[] = [];
|
28
|
+
|
29
|
+
constructor() {
|
30
|
+
this.client = new OrangebeardAsyncV3Client();
|
31
|
+
this.config = this.client.config;
|
32
|
+
}
|
33
|
+
|
34
|
+
onBegin(): void {
|
35
|
+
this.testRunId = this.client.startTestRun({
|
36
|
+
testSetName: this.config.testset,
|
37
|
+
description: this.config.description,
|
38
|
+
startTime: getTime(),
|
39
|
+
attributes: this.config.attributes
|
40
|
+
})
|
41
|
+
}
|
42
|
+
|
43
|
+
async onEnd(): Promise<void> {
|
44
|
+
await Promise.all(this.promises)
|
45
|
+
return this.client.finishTestRun(this.testRunId, {endTime: getTime()})
|
46
|
+
}
|
47
|
+
|
48
|
+
onStdErr(chunk: string | Buffer, test: void | TestCase, _result: void | TestResult): void {
|
49
|
+
//log error level
|
50
|
+
|
51
|
+
if (typeof test === 'object' && test !== null) {
|
52
|
+
const testUUID = this.tests.get(test.id);
|
53
|
+
const message = chunk.toString();
|
54
|
+
this.client.log({
|
55
|
+
logFormat: LogFormat.PLAIN_TEXT,
|
56
|
+
logLevel: LogLevel.ERROR,
|
57
|
+
logTime: getTime(),
|
58
|
+
message: message,
|
59
|
+
testRunUUID: this.testRunId,
|
60
|
+
testUUID: testUUID
|
61
|
+
});
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
onStdOut(chunk: string | Buffer, test: void | TestCase, _result: void | TestResult): void {
|
66
|
+
if (typeof test === 'object' && test !== null) {
|
67
|
+
const testUUID = this.tests.get(test.id);
|
68
|
+
const message = chunk.toString();
|
69
|
+
this.client.log({
|
70
|
+
logFormat: LogFormat.PLAIN_TEXT,
|
71
|
+
logLevel: LogLevel.INFO,
|
72
|
+
logTime: getTime(),
|
73
|
+
message: message,
|
74
|
+
testRunUUID: this.testRunId,
|
75
|
+
testUUID: testUUID
|
76
|
+
});
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {
|
81
|
+
//start step
|
82
|
+
const testUUID = this.tests.get(test.id);
|
83
|
+
|
84
|
+
const stepUUID = this.client.startStep({
|
85
|
+
startTime: getTime(),
|
86
|
+
stepName: step.title,
|
87
|
+
description: step.location ? `${path.basename(step.location.file)}:${step.location.line}`: undefined,
|
88
|
+
testRunUUID: this.testRunId,
|
89
|
+
testUUID: testUUID,
|
90
|
+
parentStepUUID: step.parent ? this.steps.get(test.id + "|" + step.parent.titlePath()) : undefined,
|
91
|
+
})
|
92
|
+
this.steps.set(test.id + "|" + step.titlePath(), stepUUID)
|
93
|
+
|
94
|
+
if(step.location) {
|
95
|
+
this.client.log({
|
96
|
+
logFormat: LogFormat.MARKDOWN,
|
97
|
+
logLevel: LogLevel.INFO,
|
98
|
+
logTime: getTime(),
|
99
|
+
message: getCodeSnippet(step.location.file, step.location.line),
|
100
|
+
testRunUUID: this.testRunId,
|
101
|
+
testUUID: testUUID,
|
102
|
+
stepUUID: stepUUID
|
103
|
+
});
|
104
|
+
}
|
105
|
+
}
|
106
|
+
|
107
|
+
onStepEnd(test: TestCase, _result: TestResult, step: TestStep): void {
|
108
|
+
const testUUID = this.tests.get(test.id);
|
109
|
+
const stepUUID = this.steps.get(test.id + "|" + step.titlePath())
|
110
|
+
if(step.error) {
|
111
|
+
const message = step.error.message;
|
112
|
+
this.client.log({
|
113
|
+
logFormat: LogFormat.MARKDOWN,
|
114
|
+
logLevel: LogLevel.ERROR,
|
115
|
+
logTime: getTime(),
|
116
|
+
message: ansiToMarkdown(message),
|
117
|
+
testRunUUID: this.testRunId,
|
118
|
+
testUUID: testUUID,
|
119
|
+
stepUUID: stepUUID
|
120
|
+
});
|
121
|
+
|
122
|
+
if (step.error.snippet) {
|
123
|
+
this.client.log({
|
124
|
+
logFormat: LogFormat.MARKDOWN,
|
125
|
+
logLevel: LogLevel.ERROR,
|
126
|
+
logTime: getTime(),
|
127
|
+
message: `\`\`\`js\n${removeAnsi(step.error.snippet)}\n\`\`\``,
|
128
|
+
testRunUUID: this.testRunId,
|
129
|
+
testUUID: testUUID,
|
130
|
+
stepUUID: stepUUID
|
131
|
+
});
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
this.client.finishStep(this.steps.get(test.id + "|" + step.titlePath()), {
|
136
|
+
endTime: getTime(),
|
137
|
+
status: step.error ? Status.FAILED : Status.PASSED,
|
138
|
+
testRunUUID: this.testRunId
|
139
|
+
})
|
140
|
+
this.steps.delete(test.id + "|" + step.titlePath())
|
141
|
+
}
|
142
|
+
|
143
|
+
onTestBegin(test: TestCase): void {
|
144
|
+
//check suite
|
145
|
+
const suiteUUID = this.getOrStartSuite(test.parent.titlePath())
|
146
|
+
const attributes: Array<Attribute> = [];
|
147
|
+
for (const tag of test.tags) {
|
148
|
+
attributes.push({value: tag})
|
149
|
+
}
|
150
|
+
const testUUID = this.client.startTest({
|
151
|
+
testType: TestType.TEST,
|
152
|
+
testRunUUID: this.testRunId,
|
153
|
+
suiteUUID: suiteUUID,
|
154
|
+
testName: test.title,
|
155
|
+
startTime: getTime(),
|
156
|
+
description: this.getTestDescription(test),
|
157
|
+
attributes: attributes
|
158
|
+
});
|
159
|
+
this.tests.set(test.id, testUUID);
|
160
|
+
}
|
161
|
+
|
162
|
+
async onTestEnd(test: TestCase, result: TestResult): Promise<void> {
|
163
|
+
const testUUID = this.tests.get(test.id);
|
164
|
+
if (result.attachments.length > 0) {
|
165
|
+
let message = "";
|
166
|
+
for (const attachment of result.attachments) {
|
167
|
+
message += `- ${attachment.name} (${attachment.contentType})\n`
|
168
|
+
}
|
169
|
+
const attachmentsLogUUID = this.client.log({
|
170
|
+
logFormat: LogFormat.MARKDOWN,
|
171
|
+
logLevel: LogLevel.INFO,
|
172
|
+
logTime: getTime(),
|
173
|
+
message: message,
|
174
|
+
testRunUUID: this.testRunId,
|
175
|
+
testUUID: testUUID
|
176
|
+
})
|
177
|
+
for (const attachment of result.attachments) {
|
178
|
+
this.promises.push(this.logAttachment(attachment, testUUID, attachmentsLogUUID));
|
179
|
+
}
|
180
|
+
}
|
181
|
+
|
182
|
+
//determine status
|
183
|
+
const status = testStatusMap[result.status]
|
184
|
+
|
185
|
+
//finish test
|
186
|
+
this.client.finishTest(testUUID, {
|
187
|
+
testRunUUID: this.testRunId,
|
188
|
+
status: status,
|
189
|
+
endTime: getTime()
|
190
|
+
});
|
191
|
+
this.tests.delete(test.id);
|
192
|
+
}
|
193
|
+
|
194
|
+
printsToStdio(): boolean {
|
195
|
+
return false;
|
196
|
+
}
|
197
|
+
|
198
|
+
private getOrStartSuite(suitePath: Array<string>): UUID {
|
199
|
+
const filteredSuitePath = suitePath.filter(name => name !== "");
|
200
|
+
let currentPath: Array<string> = [];
|
201
|
+
let parentSuiteUUID: UUID | undefined = undefined;
|
202
|
+
|
203
|
+
for (const suiteName of filteredSuitePath) {
|
204
|
+
currentPath.push(suiteName);
|
205
|
+
const existingSuiteUUID = this.suites.get(currentPath.join('|'));
|
206
|
+
|
207
|
+
if (existingSuiteUUID) {
|
208
|
+
parentSuiteUUID = existingSuiteUUID;
|
209
|
+
} else {
|
210
|
+
const newSuitesUUIDs = this.client.startSuite({
|
211
|
+
testRunUUID: this.testRunId,
|
212
|
+
parentSuiteUUID: parentSuiteUUID,
|
213
|
+
suiteNames: [suiteName],
|
214
|
+
});
|
215
|
+
|
216
|
+
if (newSuitesUUIDs && newSuitesUUIDs.length > 0) {
|
217
|
+
parentSuiteUUID = newSuitesUUIDs[0];
|
218
|
+
this.suites.set(currentPath.join('|'), parentSuiteUUID);
|
219
|
+
} else {
|
220
|
+
console.error(`Failed to create suite for path: ${currentPath.join(' > ')}`);
|
221
|
+
}
|
222
|
+
}
|
223
|
+
}
|
224
|
+
return parentSuiteUUID as UUID;
|
225
|
+
}
|
226
|
+
|
227
|
+
private getTestDescription(test: TestCase): string {
|
228
|
+
let description = `${path.basename(test.location.file)}:${test.location.line}\n`;
|
229
|
+
for (const annotation of test.annotations) {
|
230
|
+
description = `${description + annotation.type}: ${annotation.description}\n`
|
231
|
+
}
|
232
|
+
return description;
|
233
|
+
}
|
234
|
+
|
235
|
+
private async logAttachment(attachment: {
|
236
|
+
name: string,
|
237
|
+
path?: string,
|
238
|
+
body?: Buffer,
|
239
|
+
contentType: string
|
240
|
+
}, testUUID: UUID, logUUID: UUID) {
|
241
|
+
let content: Buffer;
|
242
|
+
if (attachment.body) {
|
243
|
+
content = attachment.body;
|
244
|
+
} else if (attachment.path) {
|
245
|
+
content = await getBytes(attachment.path);
|
246
|
+
} else {
|
247
|
+
throw new Error("Attachment must have either body or path defined.");
|
248
|
+
}
|
249
|
+
|
250
|
+
const orangebeardAttachment: Attachment = {
|
251
|
+
file: {
|
252
|
+
name: path.basename(attachment.path),
|
253
|
+
content: content,
|
254
|
+
contentType: attachment.contentType,
|
255
|
+
},
|
256
|
+
metaData: {
|
257
|
+
testRunUUID: this.testRunId,
|
258
|
+
testUUID: testUUID,
|
259
|
+
logUUID: logUUID,
|
260
|
+
attachmentTime: getTime()
|
261
|
+
},
|
262
|
+
};
|
263
|
+
this.client.sendAttachment(orangebeardAttachment);
|
264
|
+
}
|
265
|
+
}
|
@@ -0,0 +1,160 @@
|
|
1
|
+
import {ZonedDateTime} from "@js-joda/core";
|
2
|
+
import {FinishTest} from "@orangebeard-io/javascript-client/dist/client/models/FinishTest";
|
3
|
+
import * as fs from "node:fs";
|
4
|
+
import {promisify} from "util";
|
5
|
+
import Status = FinishTest.Status;
|
6
|
+
|
7
|
+
const stat = promisify(fs.stat);
|
8
|
+
const access = promisify(fs.access);
|
9
|
+
|
10
|
+
export function getTime() {
|
11
|
+
return ZonedDateTime.now().withFixedOffsetZone().toString();
|
12
|
+
}
|
13
|
+
|
14
|
+
export const testStatusMap = {
|
15
|
+
"passed": Status.PASSED,
|
16
|
+
"failed": Status.FAILED,
|
17
|
+
"timedOut": Status.TIMED_OUT,
|
18
|
+
"skipped": Status.SKIPPED,
|
19
|
+
"interrupted": Status.STOPPED
|
20
|
+
};
|
21
|
+
|
22
|
+
export function removeAnsi(ansiString: string): string {
|
23
|
+
const parts = ansiString.split(/(\u001b\[[0-9;]*[mG])/);
|
24
|
+
let result = "";
|
25
|
+
for (const part of parts) {
|
26
|
+
if (!part.startsWith("\u001b[")) {
|
27
|
+
result += part;
|
28
|
+
}
|
29
|
+
}
|
30
|
+
return result;
|
31
|
+
}
|
32
|
+
|
33
|
+
export function ansiToMarkdown(ansiString: string): string {
|
34
|
+
let markdown = "";
|
35
|
+
let currentStyle: { italic?: boolean, code?: boolean } = {};
|
36
|
+
|
37
|
+
const ansiCodes = {
|
38
|
+
"31": {italic: true},
|
39
|
+
"32": {italic: true},
|
40
|
+
"39": {italic: false}, // Reset styles
|
41
|
+
"2": {code: true},
|
42
|
+
"22": {code: false},
|
43
|
+
};
|
44
|
+
|
45
|
+
const parts = ansiString.split(/(\u001b\[[0-9;]*[mG])/);
|
46
|
+
|
47
|
+
for (const part of parts) {
|
48
|
+
if (part.startsWith("\u001b[")) {
|
49
|
+
const code = part.slice(2, -1);
|
50
|
+
const codes = code.split(';');
|
51
|
+
for (const c of codes) {
|
52
|
+
const style = ansiCodes[c as keyof typeof ansiCodes]; // Type guard
|
53
|
+
if (style) {
|
54
|
+
currentStyle = {...currentStyle, ...style};
|
55
|
+
}
|
56
|
+
}
|
57
|
+
} else {
|
58
|
+
let formattedPart = part.replace(/\n/g, " \n");
|
59
|
+
|
60
|
+
if (currentStyle.italic) {
|
61
|
+
formattedPart = formattedPart.endsWith(" ") ? `*${formattedPart.trim()}* ` : `*${formattedPart}*`;
|
62
|
+
|
63
|
+
}
|
64
|
+
if (currentStyle.code) {
|
65
|
+
formattedPart = `${formattedPart}`;
|
66
|
+
}
|
67
|
+
|
68
|
+
markdown += formattedPart
|
69
|
+
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
return markdown;
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* Reads a 3-line snippet from a file, centered around the specified line number.
|
78
|
+
*
|
79
|
+
* @param filePath - The path to the file.
|
80
|
+
* @param lineNumber - The line number to center the snippet around (1-based index).
|
81
|
+
* @returns A promise that resolves with the 3-line snippet or an error message if the line is out of range.
|
82
|
+
*/
|
83
|
+
export /**
|
84
|
+
* Reads a 3-line snippet from a file, centered around the specified line number.
|
85
|
+
*
|
86
|
+
* @param filePath - The path to the file.
|
87
|
+
* @param lineNumber - The line number to center the snippet around (1-based index).
|
88
|
+
* @returns The 3-line snippet or an error message if the line is out of range.
|
89
|
+
*/
|
90
|
+
function getCodeSnippet(filePath: string, lineNumber: number): string {
|
91
|
+
if (lineNumber < 1) {
|
92
|
+
throw new Error('Line number must be 1 or greater.');
|
93
|
+
}
|
94
|
+
|
95
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
96
|
+
const lines = fileContent.split(/\r?\n/); // Support both Unix and Windows line endings
|
97
|
+
|
98
|
+
const startLine = Math.max(0, lineNumber - 2); // Zero-based index for one line before
|
99
|
+
const endLine = Math.min(lines.length, lineNumber + 1); // One line after
|
100
|
+
|
101
|
+
if (startLine >= lines.length) {
|
102
|
+
throw new Error('Line number is out of range.');
|
103
|
+
}
|
104
|
+
|
105
|
+
let snippet = lines.slice(startLine, endLine);
|
106
|
+
if (snippet.length > 0 && snippet[0].trim() === "") {
|
107
|
+
snippet = snippet.slice(1);
|
108
|
+
}
|
109
|
+
|
110
|
+
return `\`\`\`js\n${snippet.join('\n')}\n\`\`\``;
|
111
|
+
}
|
112
|
+
|
113
|
+
const fileExists = async (filepath: string) => {
|
114
|
+
try {
|
115
|
+
await access(filepath, fs.constants.F_OK);
|
116
|
+
return true;
|
117
|
+
} catch {
|
118
|
+
return false;
|
119
|
+
}
|
120
|
+
};
|
121
|
+
|
122
|
+
const waitForFile = async (filepath: string, interval = 1000, timeout = 60000) => {
|
123
|
+
const start = Date.now();
|
124
|
+
|
125
|
+
while (true) {
|
126
|
+
const now = Date.now();
|
127
|
+
if (now - start > timeout) {
|
128
|
+
throw new Error(`Timeout: ${filepath} did not become available within ${timeout}ms`);
|
129
|
+
}
|
130
|
+
|
131
|
+
if (await fileExists(filepath)) {
|
132
|
+
const stats = [];
|
133
|
+
for (let i = 0; i < 2; i++) {
|
134
|
+
stats.push(await stat(filepath));
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
136
|
+
}
|
137
|
+
|
138
|
+
const [first, second] = stats;
|
139
|
+
if (
|
140
|
+
first.mtimeMs === second.mtimeMs &&
|
141
|
+
first.size === second.size
|
142
|
+
) {
|
143
|
+
return;
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
148
|
+
}
|
149
|
+
};
|
150
|
+
|
151
|
+
export const getBytes = async (filePath: string) => {
|
152
|
+
try {
|
153
|
+
await waitForFile(filePath, 100, 5000)
|
154
|
+
return fs.readFileSync(filePath);
|
155
|
+
} catch (err) {
|
156
|
+
console.error('Error reading file:', err);
|
157
|
+
throw err;
|
158
|
+
}
|
159
|
+
};
|
160
|
+
|
package/tsconfig.json
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "es6",
|
4
|
+
"module": "commonjs",
|
5
|
+
"allowJs": false,
|
6
|
+
"outDir": "dist",
|
7
|
+
"rootDir": "src",
|
8
|
+
"noImplicitAny": true,
|
9
|
+
"esModuleInterop": true,
|
10
|
+
"resolveJsonModule": true,
|
11
|
+
"moduleResolution": "node",
|
12
|
+
"baseUrl": ".",
|
13
|
+
"paths": {
|
14
|
+
"*": ["node_modules/*"]
|
15
|
+
},
|
16
|
+
"typeRoots": ["./node_modules/@types"]
|
17
|
+
},
|
18
|
+
"include": ["./src/**/*"],
|
19
|
+
"exclude": ["node_modules"],
|
20
|
+
}
|