@deployanyway/error-translator 0.0.0-stage → 0.1.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +142 -3
- package/bin/cli.js +38 -0
- package/package.json +49 -4
- package/src/definitions.js +83 -0
- package/src/index.js +93 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- Correct npm installation and npx documentation after the initial publication.
|
|
6
|
+
- Add a searchable, humorous package description and relevant npm keywords.
|
|
7
|
+
- No API, CLI behavior, or dependency changes.
|
|
8
|
+
|
|
9
|
+
## 0.1.0 — 2026-10-08
|
|
10
|
+
|
|
11
|
+
- Plain-English translations for ECONNREFUSED, ENOENT, EADDRINUSE, MODULE_NOT_FOUND, ERR_MODULE_NOT_FOUND, and TypeError.
|
|
12
|
+
- Structured results, text rendering, and CLI with JSON output.
|
|
13
|
+
- Automated tests, linting, formatting, and Node 22/24 CI.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deploy Anyway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# error-translator
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
Connection refused
|
|
7
|
+
|
|
8
|
+
What happened:
|
|
9
|
+
The application tried to connect to a service, but nothing accepted the connection.
|
|
10
|
+
|
|
11
|
+
Likely causes:
|
|
12
|
+
- The service is not running
|
|
13
|
+
- The host or port is incorrect
|
|
14
|
+
- A firewall actively rejected the connection
|
|
15
|
+
|
|
16
|
+
Try:
|
|
17
|
+
- Confirm that the service is running and listening on the configured host and port.
|
|
18
|
+
- Check connection settings and firewall rules.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Available on npm. Requires Node 22 or later.
|
|
24
|
+
You can also run from source with Node 22 or 24:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git clone https://github.com/DeployAnyway/error-translator.git
|
|
28
|
+
cd error-translator
|
|
29
|
+
git checkout main
|
|
30
|
+
npm ci
|
|
31
|
+
node bin/cli.js ECONNREFUSED
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install from npm:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npm install @deployanyway/error-translator
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import {
|
|
44
|
+
translateError,
|
|
45
|
+
renderTranslation,
|
|
46
|
+
} from "@deployanyway/error-translator";
|
|
47
|
+
|
|
48
|
+
const result = translateError("ECONNREFUSED");
|
|
49
|
+
console.log(result.suggestions);
|
|
50
|
+
console.log(renderTranslation(result));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## CLI example
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
node bin/cli.js "TypeError: value is not a function" --json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Run with npx: `npx @deployanyway/error-translator ECONNREFUSED`.
|
|
60
|
+
|
|
61
|
+
## API and options
|
|
62
|
+
|
|
63
|
+
### `translateError(error, { mode = 'plain' } = {})`
|
|
64
|
+
|
|
65
|
+
Accepts a nonempty string, an Error, or an object with string `code`, `name`,
|
|
66
|
+
or `message` fields. Returns a fresh structured object with `code`, `title`,
|
|
67
|
+
`explanation`, `likelyCauses` (string array), `suggestions` (string array), and `mode`.
|
|
68
|
+
|
|
69
|
+
An explicit code takes precedence, followed by a recognized name, then a
|
|
70
|
+
case-sensitive whole-word match in the message. Unknown errors return general
|
|
71
|
+
guidance with code `UNKNOWN`, or preserve an explicit unknown code.
|
|
72
|
+
Invalid input throws TypeError; unsupported modes throw RangeError.
|
|
73
|
+
Only `plain` is available in this MVP. Humorous modes are planned.
|
|
74
|
+
|
|
75
|
+
### `renderTranslation(result)`
|
|
76
|
+
|
|
77
|
+
Returns readable text. Does not print, mutate the result, or exit the process.
|
|
78
|
+
Invalid result shapes throw TypeError.
|
|
79
|
+
|
|
80
|
+
### Built-in errors
|
|
81
|
+
|
|
82
|
+
| Code/name | Guidance |
|
|
83
|
+
| -------------------- | ------------------------------------------ |
|
|
84
|
+
| ECONNREFUSED | Service availability, host, port, firewall |
|
|
85
|
+
| ENOENT | Missing paths and working directories |
|
|
86
|
+
| EADDRINUSE | Port conflicts and duplicate instances |
|
|
87
|
+
| MODULE_NOT_FOUND | CommonJS dependencies and paths |
|
|
88
|
+
| ERR_MODULE_NOT_FOUND | ES module dependencies, paths, extensions |
|
|
89
|
+
| TypeError | Unexpected types and null/undefined values |
|
|
90
|
+
|
|
91
|
+
Translations describe likely causes, not a diagnosis. Keep the original error
|
|
92
|
+
and stack trace for context. No AI API or remote service is used.
|
|
93
|
+
|
|
94
|
+
## CLI reference
|
|
95
|
+
|
|
96
|
+
`error-translator <code or message> [options]`
|
|
97
|
+
|
|
98
|
+
| Option | Behavior |
|
|
99
|
+
| ----------------- | --------------------- |
|
|
100
|
+
| `--help`, `-h` | Show usage |
|
|
101
|
+
| `--version`, `-v` | Show package version |
|
|
102
|
+
| `--json` | Print structured JSON |
|
|
103
|
+
| `--mode plain` | Select plain mode |
|
|
104
|
+
|
|
105
|
+
Quote messages containing spaces or shell punctuation. Use `--` before a message
|
|
106
|
+
beginning with a dash. No stdin support is included in the MVP.
|
|
107
|
+
Exit code 0 means translation/help/version succeeded, including unknown errors.
|
|
108
|
+
Exit code 2 means invalid arguments. The library never exits.
|
|
109
|
+
|
|
110
|
+
## Examples and development
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npm ci
|
|
114
|
+
node examples/basic.js
|
|
115
|
+
npm test
|
|
116
|
+
npm run lint
|
|
117
|
+
npm run format:check
|
|
118
|
+
npm pack --dry-run
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
ES modules, Node's test runner, zero production dependencies. CI checks Node 22
|
|
122
|
+
and 24. Error definitions live in `src/definitions.js`; API and renderer in
|
|
123
|
+
`src/index.js`; CLI in `bin/cli.js`. Only runtime files, README, MIT license,
|
|
124
|
+
changelog, and package metadata are packaged.
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Accurate, actionable guidance and tests
|
|
129
|
+
are welcome.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
[MIT](LICENSE).
|
|
134
|
+
|
|
135
|
+
## More from DeployAnyway
|
|
136
|
+
|
|
137
|
+
**Tools for developers who probably know better.**
|
|
138
|
+
|
|
139
|
+
- [error-translator](https://github.com/DeployAnyway/error-translator)
|
|
140
|
+
- [excuse-js](https://github.com/DeployAnyway/excuse-js)
|
|
141
|
+
- [doggo-log](https://github.com/DeployAnyway/doggo-log)
|
|
142
|
+
- [ship-it-meter](https://github.com/DeployAnyway/ship-it-meter)
|
|
143
|
+
- [bro-say](https://github.com/DeployAnyway/bro-say)
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { translateError, renderTranslation } from "../src/index.js";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const { values, positionals } = parseArgs({
|
|
9
|
+
allowPositionals: true,
|
|
10
|
+
options: {
|
|
11
|
+
help: { type: "boolean", short: "h" },
|
|
12
|
+
version: { type: "boolean", short: "v" },
|
|
13
|
+
json: { type: "boolean" },
|
|
14
|
+
mode: { type: "string", default: "plain" },
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (values.help) {
|
|
18
|
+
console.log(
|
|
19
|
+
'Usage: error-translator <error code or quoted message> [--json] [--mode plain]\n\nTranslate errors into human-readable guidance.\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --json Print a structured JSON result\n --mode plain Translation mode (MVP: plain only)\n\nExamples:\n error-translator ECONNREFUSED\n error-translator "TypeError: value is not a function" --json\n\nExit codes: 0 translation/help/version; 2 invalid arguments.',
|
|
20
|
+
);
|
|
21
|
+
} else if (values.version) {
|
|
22
|
+
console.log(
|
|
23
|
+
JSON.parse(
|
|
24
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
25
|
+
).version,
|
|
26
|
+
);
|
|
27
|
+
} else {
|
|
28
|
+
const result = translateError(positionals.join(" "), { mode: values.mode });
|
|
29
|
+
console.log(
|
|
30
|
+
values.json ? JSON.stringify(result, null, 2) : renderTranslation(result),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(
|
|
35
|
+
`error-translator: ${error.message}\nRun with --help for usage.`,
|
|
36
|
+
);
|
|
37
|
+
process.exitCode = 2;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployanyway/error-translator",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Plain-English Node.js error explanations and debugging tips, because the stack trace has chosen violence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"error-translator": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check ."
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/DeployAnyway/error-translator.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/DeployAnyway/error-translator/issues"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"eslint": "^10.12.0",
|
|
39
|
+
"prettier": "^3.6.2"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"nodejs",
|
|
43
|
+
"javascript",
|
|
44
|
+
"cli",
|
|
45
|
+
"developer-tools",
|
|
46
|
+
"debugging",
|
|
47
|
+
"error-messages",
|
|
48
|
+
"error-translator",
|
|
49
|
+
"plain-english"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Keep technical guidance here so contributors can add errors without changing logic.
|
|
2
|
+
export const definitions = {
|
|
3
|
+
ECONNREFUSED: {
|
|
4
|
+
title: "Connection refused",
|
|
5
|
+
explanation:
|
|
6
|
+
"The application tried to connect to a service, but nothing accepted the connection.",
|
|
7
|
+
likelyCauses: [
|
|
8
|
+
"The service is not running",
|
|
9
|
+
"The host or port is incorrect",
|
|
10
|
+
"A firewall actively rejected the connection",
|
|
11
|
+
],
|
|
12
|
+
suggestions: [
|
|
13
|
+
"Confirm that the service is running and listening on the configured host and port.",
|
|
14
|
+
"Check connection settings and firewall rules.",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
ENOENT: {
|
|
18
|
+
title: "File or directory not found",
|
|
19
|
+
explanation:
|
|
20
|
+
"The application tried to use a filesystem path that does not exist.",
|
|
21
|
+
likelyCauses: [
|
|
22
|
+
"The path is misspelled",
|
|
23
|
+
"The file was moved or deleted",
|
|
24
|
+
"A relative path is being resolved from an unexpected working directory",
|
|
25
|
+
],
|
|
26
|
+
suggestions: [
|
|
27
|
+
"Check the full path and the current working directory.",
|
|
28
|
+
"Create the required file or directory if it should exist.",
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
EADDRINUSE: {
|
|
32
|
+
title: "Address already in use",
|
|
33
|
+
explanation:
|
|
34
|
+
"The application tried to listen on an address and port that another listener already uses.",
|
|
35
|
+
likelyCauses: [
|
|
36
|
+
"Another instance of the application is running",
|
|
37
|
+
"Another service uses the same port",
|
|
38
|
+
],
|
|
39
|
+
suggestions: [
|
|
40
|
+
"Identify the process using the port before stopping it.",
|
|
41
|
+
"Choose a different available port or stop the duplicate instance.",
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
MODULE_NOT_FOUND: {
|
|
45
|
+
title: "Module not found",
|
|
46
|
+
explanation:
|
|
47
|
+
"Node.js could not resolve a module requested by the application.",
|
|
48
|
+
likelyCauses: [
|
|
49
|
+
"A dependency is missing",
|
|
50
|
+
"The module path is incorrect",
|
|
51
|
+
"A dependency references a missing module",
|
|
52
|
+
],
|
|
53
|
+
suggestions: [
|
|
54
|
+
"Check the missing module name and the require stack.",
|
|
55
|
+
"Install declared dependencies with npm install and verify local paths.",
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
ERR_MODULE_NOT_FOUND: {
|
|
59
|
+
title: "ES module not found",
|
|
60
|
+
explanation: "Node.js could not resolve an imported ES module.",
|
|
61
|
+
likelyCauses: [
|
|
62
|
+
"A dependency is missing",
|
|
63
|
+
"The import path or file extension is incorrect",
|
|
64
|
+
],
|
|
65
|
+
suggestions: [
|
|
66
|
+
"Install declared dependencies and verify the import path.",
|
|
67
|
+
"Include the file extension for relative Node.js ES module imports.",
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
TypeError: {
|
|
71
|
+
title: "Unexpected value type",
|
|
72
|
+
explanation:
|
|
73
|
+
"An operation received a value it cannot use, such as calling a non-function or reading a property of undefined.",
|
|
74
|
+
likelyCauses: [
|
|
75
|
+
"A value is null or undefined",
|
|
76
|
+
"A value has a different type than expected",
|
|
77
|
+
],
|
|
78
|
+
suggestions: [
|
|
79
|
+
"Inspect the value at the first relevant line in the stack trace.",
|
|
80
|
+
"Check inputs and return values before accessing properties or calling functions.",
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { definitions } from "./definitions.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Translate a nonempty string, Error, or error-like object with a code/name/message.
|
|
5
|
+
* Explicit codes take precedence over names and message matching.
|
|
6
|
+
* @param {string | Error | {code?: string, name?: string, message?: string}} error
|
|
7
|
+
* @param {{mode?: 'plain'}} [options]
|
|
8
|
+
* @returns {{code: string, title: string, explanation: string, likelyCauses: string[], suggestions: string[], mode: string}}
|
|
9
|
+
*/
|
|
10
|
+
export function translateError(error, options = {}) {
|
|
11
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
12
|
+
throw new TypeError("Options must be an object.");
|
|
13
|
+
}
|
|
14
|
+
const mode = options.mode ?? "plain";
|
|
15
|
+
if (mode !== "plain") throw new RangeError("Supported modes: plain.");
|
|
16
|
+
let code;
|
|
17
|
+
let name;
|
|
18
|
+
let message;
|
|
19
|
+
if (typeof error === "string") {
|
|
20
|
+
message = error.trim();
|
|
21
|
+
} else if (error && typeof error === "object" && !Array.isArray(error)) {
|
|
22
|
+
for (const field of ["code", "name", "message"]) {
|
|
23
|
+
if (error[field] !== undefined && typeof error[field] !== "string") {
|
|
24
|
+
throw new TypeError(`Error ${field} must be a string.`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
code = error.code?.trim();
|
|
28
|
+
name = error.name?.trim();
|
|
29
|
+
message = error.message?.trim();
|
|
30
|
+
} else {
|
|
31
|
+
throw new TypeError(
|
|
32
|
+
"Provide a nonempty error string or error-like object.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!code && !name && !message)
|
|
36
|
+
throw new TypeError(
|
|
37
|
+
"Provide a nonempty error string or error-like object.",
|
|
38
|
+
);
|
|
39
|
+
const matched =
|
|
40
|
+
code ||
|
|
41
|
+
(Object.hasOwn(definitions, name ?? "") ? name : undefined) ||
|
|
42
|
+
Object.keys(definitions).find((key) =>
|
|
43
|
+
new RegExp(`\\b${key}\\b`).test(message ?? ""),
|
|
44
|
+
);
|
|
45
|
+
const definition = Object.hasOwn(definitions, matched ?? "")
|
|
46
|
+
? definitions[matched]
|
|
47
|
+
: undefined;
|
|
48
|
+
const fallback = {
|
|
49
|
+
title: "Unrecognized error",
|
|
50
|
+
explanation: "No built-in translation matches this error yet.",
|
|
51
|
+
likelyCauses: [],
|
|
52
|
+
suggestions: [
|
|
53
|
+
"Read the original error and inspect the first relevant line in its stack trace.",
|
|
54
|
+
"Check the documentation for the component that reported the error.",
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
const result = definition ?? fallback;
|
|
58
|
+
return {
|
|
59
|
+
code: matched ?? "UNKNOWN",
|
|
60
|
+
...result,
|
|
61
|
+
likelyCauses: [...result.likelyCauses],
|
|
62
|
+
suggestions: [...result.suggestions],
|
|
63
|
+
mode,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Render a structured translation as readable text without logging or exiting. */
|
|
68
|
+
export function renderTranslation(result) {
|
|
69
|
+
if (
|
|
70
|
+
!result ||
|
|
71
|
+
typeof result.title !== "string" ||
|
|
72
|
+
typeof result.explanation !== "string" ||
|
|
73
|
+
!Array.isArray(result.likelyCauses) ||
|
|
74
|
+
!Array.isArray(result.suggestions) ||
|
|
75
|
+
![...result.likelyCauses, ...result.suggestions].every(
|
|
76
|
+
(item) => typeof item === "string",
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
throw new TypeError("Provide a structured translation result.");
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
result.title,
|
|
83
|
+
`What happened:\n${result.explanation}`,
|
|
84
|
+
...(result.likelyCauses.length
|
|
85
|
+
? [
|
|
86
|
+
`Likely causes:\n${result.likelyCauses.map((item) => `- ${item}`).join("\n")}`,
|
|
87
|
+
]
|
|
88
|
+
: []),
|
|
89
|
+
...(result.suggestions.length
|
|
90
|
+
? [`Try:\n${result.suggestions.map((item) => `- ${item}`).join("\n")}`]
|
|
91
|
+
: []),
|
|
92
|
+
].join("\n\n");
|
|
93
|
+
}
|