ssh-client-wrapper 2.13.2 → 2.15.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/README.md CHANGED
@@ -96,6 +96,40 @@ Downloads files or directories from the remote host using `rsync`.
96
96
  * `dst` (string): The local destination path.
97
97
  * Returns: `Promise<void>`
98
98
 
99
+ ### `.remoteToRemoteCopy(src, dstHostInfo, dst, [opt], [timeout])`
100
+ Copies files or directories directly from this remote host to another remote host using `rsync` with SSH agent forwarding.
101
+
102
+ * `src` (string[]): An array of file or directory paths on this (source) remote host.
103
+ * `dstHostInfo` (Object): Destination host information (requires `host` property; optionally `user`, `port`, `noStrictHostKeyChecking`).
104
+ * `dst` (string): Destination path on the destination remote host.
105
+ * `opt` (string[], optional): Additional options for rsync (e.g., `["--exclude=*.log"]`).
106
+ * `timeout` (number, optional): Timeout in seconds.
107
+ * Returns: `Promise<void>`
108
+
109
+ **Note:** This method requires SSH agent forwarding to be enabled. Your SSH key must be loaded in the SSH agent on localhost, and the connection to the source host must forward the agent. No password or keyFile is needed for the source → destination connection as it uses the forwarded SSH agent.
110
+
111
+ **Example:**
112
+ ```javascript
113
+ const srcHost = new SshClientWrapper({
114
+ host: 'source.server.com',
115
+ user: 'user1',
116
+ keyFile: '/path/to/key'
117
+ });
118
+
119
+ const dstHostInfo = {
120
+ host: 'destination.server.com',
121
+ user: 'user2'
122
+ };
123
+
124
+ // Copy files directly from source to destination
125
+ await srcHost.remoteToRemoteCopy(
126
+ ['/remote/path/file.txt', '/remote/path/dir/'],
127
+ dstHostInfo,
128
+ '/destination/path/',
129
+ ['--exclude=*.tmp']
130
+ );
131
+ ```
132
+
99
133
  ### `.canConnect([timeout])`
100
134
  Checks if a connection to the remote host can be established.
101
135
 
package/lib/index.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ declare module "ssh-client-wrapper" {
2
+ type HostInfo = {
3
+ host: string;
4
+ user: string;
5
+ port?: number;
6
+ password?: string | (() => string);
7
+ passphrase?: string | (() => string);
8
+ keyFile?: string;
9
+ noStrictHostKeyChecking?: boolean;
10
+ ControlPersist?: number;
11
+ ConnectTimeout?: number;
12
+ maxRetry?: number;
13
+ retryDuration?: number;
14
+ rcfile?: string;
15
+ prependCmd?: string;
16
+ sshOpt?: string[];
17
+ };
18
+
19
+ class SshClientWrapper {
20
+ constructor(hostInfo: HostInfo);
21
+ exec(
22
+ cmd: string,
23
+ timeout?: number,
24
+ outputCallback?: (data: string) => void,
25
+ rcfile?: string,
26
+ prependCmd?: string
27
+ ): Promise<number>;
28
+ execAndGetOutput(
29
+ cmd: string,
30
+ timeout?: number,
31
+ rcfile?: string,
32
+ prependCmd?: string
33
+ ): Promise<{ output: string[]; rt: number }>;
34
+ ls(
35
+ target: string,
36
+ lsOpt?: string[],
37
+ timeout?: number
38
+ ): Promise<string[] | number>;
39
+ expect(
40
+ cmd: string,
41
+ expects: [string, string][],
42
+ timeout?: number
43
+ ): Promise<number>;
44
+ send(
45
+ src: string[],
46
+ dst: string,
47
+ opt?: string[],
48
+ timeout?: number
49
+ ): Promise<void>;
50
+ recv(
51
+ src: string[],
52
+ dst: string,
53
+ opt?: string[],
54
+ timeout?: number
55
+ ): Promise<void>;
56
+ canConnect(timeout?: number): Promise<boolean>;
57
+ disconnect(): Promise<void>;
58
+ }
59
+
60
+ export default SshClientWrapper;
61
+ }
package/lib/index.js CHANGED
@@ -220,6 +220,42 @@ class SshClientWrapper {
220
220
  return this.deps.canConnect(this.hostInfo, timeout);
221
221
  }
222
222
 
223
+ /**
224
+ * Copy files directly from this remote host to another remote host using SSH agent forwarding.
225
+ * @param {string[]} src - File or directory paths on this (source) remote host.
226
+ * @param {object} dstHostInfo - Destination host information (needs host and user properties).
227
+ * @param {string} dst - Destination path on the destination remote host.
228
+ * @param {string[]} opt - options for rsync
229
+ * @param {Integer} timeout - timeout in seconds
230
+ * @returns {Promise} - Resolved when file transfer is done.
231
+ *
232
+ * Note: This requires SSH agent forwarding to be available.
233
+ * The SSH agent on localhost must be forwarded to the source host,
234
+ * and then forwarded again to the destination host.
235
+ */
236
+ async remoteToRemoteCopy(src, dstHostInfo, dst, opt = [], timeout = 0) {
237
+ debug("remoteToRemoteCopy called", src, "to", dstHostInfo.host, dst);
238
+
239
+ if (!isArrayOfString(src)) {
240
+ return logAndReject("src must be array of string");
241
+ }
242
+ if (!src.some((e)=>{
243
+ return e !== "";
244
+ })) {
245
+ return logAndReject("src must contain non-empty string");
246
+ }
247
+ if (typeof dstHostInfo !== "object" || !dstHostInfo.host) {
248
+ return logAndReject("dstHostInfo must be object with host property");
249
+ }
250
+ if (typeof dst !== "string" || dst === "") {
251
+ return logAndReject("dst must be non-empty string");
252
+ }
253
+ if (typeof opt !== "undefined" && !isArrayOfString(opt)) {
254
+ return logAndReject("opt must be array of string");
255
+ }
256
+ return this.deps.remoteToRemoteCopy(this.hostInfo, src, dstHostInfo, dst, opt, timeout);
257
+ }
258
+
223
259
  /**
224
260
  * Disconnect master session
225
261
  */
package/lib/rsyncExec.js CHANGED
@@ -127,3 +127,70 @@ export async function recv(hostInfo, argSrc, dst, rsyncOpt, timeout) {
127
127
  debugVerbose(`recv: ${rsyncCmd} ${args.join(" ")}`);
128
128
  return retryWrapper(fork.bind(null, hostInfo, rsyncCmd, args, timeout, null, acceptableRsyncRetrunCodes), hostInfo);
129
129
  }
130
+
131
+ /**
132
+ * Copy files directly between remote hosts using rsync over SSH with agent forwarding.
133
+ * This function executes rsync on the source remote host to transfer files directly
134
+ * to the destination remote host, avoiding the need to download and upload through localhost.
135
+ * @param {object} srcHostInfo - source host information object
136
+ * @param {string[]} argSrc - File or directory paths on the source remote host
137
+ * @param {object} dstHostInfo - destination host information object (needs host and user properties)
138
+ * @param {string} dst - Destination path on the destination remote host
139
+ * @param {string[]} rsyncOpt - optional arguments for rsync
140
+ * @param {Integer} timeout - timeout in seconds
141
+ * @returns {Promise} - Resolved when transfer is complete
142
+ *
143
+ * Authentication from source to destination uses SSH agent forwarding,
144
+ * so dstHostInfo does not need keyFile or password.
145
+ */
146
+ export async function remoteToRemoteCopy(srcHostInfo, argSrc, dstHostInfo, dst, rsyncOpt, timeout) {
147
+ await connect(srcHostInfo);
148
+
149
+ //Construct destination specification for rsync: user@host:path
150
+ let dstSpec = dstHostInfo.host;
151
+ if (dstHostInfo.user) {
152
+ dstSpec = `${dstHostInfo.user}@${dstHostInfo.host}`;
153
+ }
154
+ dstSpec = `${dstSpec}:${dst}`;
155
+
156
+ //Build SSH options for the nested SSH connection (from src to dst)
157
+ const nestedSshArgs = ["-A"]; //Enable agent forwarding
158
+
159
+ if (dstHostInfo.port) {
160
+ nestedSshArgs.push("-p", dstHostInfo.port.toString());
161
+ }
162
+ if (dstHostInfo.noStrictHostKeyChecking) {
163
+ nestedSshArgs.push("-oStrictHostKeyChecking=no");
164
+ }
165
+
166
+ //Build rsync command to be executed on the source host
167
+ const rshOpt = `-e '${sshCmd} ${nestedSshArgs.join(" ")}'`;
168
+ const rsyncOptStr = rsyncOpt.length > 0 ? rsyncOpt.join(" ") : "";
169
+ const srcPaths = argSrc.join(" ");
170
+
171
+ //Create destination directory on the destination host
172
+ let remoteMkdirCmd = "";
173
+ if (dst.endsWith("/")) {
174
+ remoteMkdirCmd = `mkdir -p ${dst}`;
175
+ } else {
176
+ const { dir } = path.parse(dst);
177
+ if (dir !== "" && dir !== "/") {
178
+ remoteMkdirCmd = `mkdir -p ${dir}`;
179
+ }
180
+ }
181
+
182
+ if (remoteMkdirCmd) {
183
+ //Execute mkdir on destination host via source host with agent forwarding
184
+ const dstUser = dstHostInfo.user ? `${dstHostInfo.user}@` : "";
185
+ const mkdirSshCmd = `${sshCmd} -A ${nestedSshArgs.filter(arg=>{return arg !== "-A";}).join(" ")} ${dstUser}${dstHostInfo.host} '${remoteMkdirCmd}'`;
186
+ debug(`creating remote directory: ${mkdirSshCmd}`);
187
+ await sshExec(srcHostInfo, mkdirSshCmd, timeout, debugVerbose);
188
+ }
189
+
190
+ //Execute rsync on source host
191
+ const rsyncRemoteCmd = `rsync -avv --copy-unsafe-links ${rshOpt} ${rsyncOptStr} ${srcPaths} ${dstSpec}`;
192
+ debug(`remote to remote copy: ${rsyncRemoteCmd}`);
193
+ debugVerbose(`executing on ${srcHostInfo.host}: ${rsyncRemoteCmd}`);
194
+
195
+ return sshExec(srcHostInfo, rsyncRemoteCmd, timeout, debugVerbose);
196
+ }
package/lib/util.js CHANGED
@@ -201,6 +201,12 @@ export const getSshOption = (hostInfo, withoutDestination)=>{
201
201
  if (hostInfo.noStrictHostKeyChecking) {
202
202
  args.push("-oStrictHostKeyChecking=no");
203
203
  }
204
+
205
+ //Enable SSH agent forwarding if keyFile is set (indicates we're using key-based auth)
206
+ if (hostInfo.keyFile) {
207
+ args.push("-A");
208
+ }
209
+
204
210
  const controlPersist = Number.isInteger(hostInfo.ControlPersist) && hostInfo.ControlPersist >= 0 ? hostInfo.ControlPersist : "180";
205
211
  const controlPersistDir = getControlPersistDir(hostInfo);
206
212
  args.push("-oControlMaster=auto");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssh-client-wrapper",
3
- "version": "2.13.2",
3
+ "version": "2.15.0",
4
4
  "description": "wrapper library for open ssh client and rsync",
5
5
  "keywords": [
6
6
  "ssh",
@@ -13,6 +13,7 @@
13
13
  ],
14
14
  "type": "module",
15
15
  "main": "lib/index.js",
16
+ "types": "lib/index.d.ts",
16
17
  "scripts": {
17
18
  "mocha": "mocha --recursive test",
18
19
  "lint": "eslint --fix ./",
@@ -37,7 +38,7 @@
37
38
  "ajv": "^8.17.1",
38
39
  "ajv-keywords": "^5.1.0",
39
40
  "debug": "^4.4.3",
40
- "glob": "^11.0.3",
41
+ "glob": "^12.0.0",
41
42
  "node-pty-prebuilt-multiarch": "^0.10.1-pre.5"
42
43
  },
43
44
  "devDependencies": {
@@ -50,6 +51,7 @@
50
51
  "c8": "^10.1.3",
51
52
  "chai": "^5.3.1",
52
53
  "chai-as-promised": "^8.0.1",
54
+ "eslint-plugin-chai-expect": "^3.1.0",
53
55
  "eslint-plugin-chai-friendly": "^1.1.0",
54
56
  "eslint-plugin-jsdoc": "^54.1.0",
55
57
  "eslint-plugin-node": "^11.1.0",
package/.dockerignore DELETED
@@ -1,2 +0,0 @@
1
- node_modules
2
- run_test_all.sh
@@ -1,11 +0,0 @@
1
- # To get started with Dependabot version updates, you'll need to specify which
2
- # package ecosystems to update and where the package manifests are located.
3
- # Please see the documentation for all configuration options:
4
- # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
-
6
- version: 2
7
- updates:
8
- - package-ecosystem: "" # See documentation for possible values
9
- directory: "/" # Location of package manifests
10
- schedule:
11
- interval: "weekly"
@@ -1,24 +0,0 @@
1
- name: report coverage data to code climate
2
- on:
3
- push:
4
- branches:
5
- - main
6
- jobs:
7
- test:
8
- uses: ./.github/workflows/run_test.yml
9
- coverage:
10
- needs:
11
- - test
12
- name: coverage report
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/download-artifact@v4
17
- with:
18
- name: code-coverage-report
19
- - uses: paambaati/codeclimate-action@v5.0.0
20
- env:
21
- CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
22
- with:
23
- coverageLocations: "coverage-report.lcov:lcov"
24
- debug: true
@@ -1,29 +0,0 @@
1
- name: Release and Publish
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
-
8
- jobs:
9
- test:
10
- uses: ./.github/workflows/run_test.yml
11
-
12
- release:
13
- needs: test
14
- runs-on: ubuntu-latest
15
- permissions:
16
- contents: write
17
- issues: write
18
- pull-requests: write
19
- steps:
20
- - uses: actions/checkout@v4
21
- - uses: actions/setup-node@v4
22
- with:
23
- node-version: 20
24
- - run: npm ci
25
- - name: Semantic Release
26
- uses: cycjimmy/semantic-release-action@v4
27
- env:
28
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,40 +0,0 @@
1
- name: run UT
2
- on:
3
- pull_request:
4
- branches:
5
- - main
6
- workflow_call:
7
- concurrency:
8
- group: ${{ github.workflow }}-${{ github.ref }}
9
- cancel-in-progress: true
10
- jobs:
11
- test:
12
- services:
13
- testbed:
14
- image: naoso5/test-sshd
15
- ports:
16
- - 10022:22
17
- env:
18
- TEST_USER: testuser
19
- TEST_PW: passw0rd
20
- TEST_PORT: 10022
21
- runs-on: ubuntu-latest
22
- strategy:
23
- matrix:
24
- node-version: [16.x, 18.x, 20.x, 22.x]
25
- steps:
26
- - uses: actions/checkout@v4
27
- - name: Use Node.js ${{ matrix.node-version }}
28
- uses: actions/setup-node@v4
29
- with:
30
- node-version: ${{ matrix.node-version }}
31
- - run: npm ci
32
- - run: npm run test
33
- - run: npm run coverage
34
- if: matrix.node-version == '22.x'
35
- - uses: actions/upload-artifact@v4
36
- if: matrix.node-version == '22.x'
37
- with:
38
- name: code-coverage-report
39
- path: coverage-report.lcov
40
- retention-days: 1
@@ -1 +0,0 @@
1
- npx lint-staged
package/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npx lint-staged
package/.releaserc.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "branches": ["main"],
3
- "plugins": [
4
- "@semantic-release/commit-analyzer",
5
- "@semantic-release/release-notes-generator",
6
- "@semantic-release/changelog",
7
- ["@semantic-release/npm", {
8
- "npmPublish": true
9
- }],
10
- ["@semantic-release/git", {
11
- "assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
12
- "message": "chore(release): ${nextRelease.version} [skip ci]\\n\\n${nextRelease.notes}"
13
- }],
14
- "@semantic-release/github"
15
- ]
16
- }
package/CHANGELOG.md DELETED
@@ -1,27 +0,0 @@
1
- ## [2.13.2](https://github.com/so5/ssh-client-wrapper/compare/v2.13.1...v2.13.2) (2025-11-04)
2
-
3
-
4
- ### Bug Fixes
5
-
6
- * upgrade debug from 4.4.1 to 4.4.3 ([787706c](https://github.com/so5/ssh-client-wrapper/commit/787706ce2ee5f1144e1d5ccb2e11b06c498df0dd))
7
-
8
- ## [2.13.1](https://github.com/so5/ssh-client-wrapper/compare/v2.13.0...v2.13.1) (2025-08-20)
9
-
10
-
11
- ### Bug Fixes
12
-
13
- * Update Code Climate workflow trigger ([7cf0a93](https://github.com/so5/ssh-client-wrapper/commit/7cf0a93913ceef762c267cb3a947c176479ce068))
14
-
15
- # [2.13.0](https://github.com/so5/ssh-client-wrapper/compare/v2.12.4...v2.13.0) (2025-08-20)
16
-
17
-
18
- ### Bug Fixes
19
-
20
- * upgrade debug from 4.4.0 to 4.4.1 ([35f966a](https://github.com/so5/ssh-client-wrapper/commit/35f966a7212d6bb9677964688bb94ae06f869ada))
21
-
22
-
23
- ### Features
24
-
25
- * Improve DX and automate releases ([805f2ea](https://github.com/so5/ssh-client-wrapper/commit/805f2ea7756f6ad61238a4a21d86575a17e748d9))
26
- * Rewrite in ESM ([c5a95c1](https://github.com/so5/ssh-client-wrapper/commit/c5a95c18401e7b373249d3707609f2a9576f7400))
27
- * Rewrite in ESM ([71ed6f5](https://github.com/so5/ssh-client-wrapper/commit/71ed6f56af43cac1471cbf4a4ed3016f7e55ab4d))
package/Dockerfile DELETED
@@ -1,7 +0,0 @@
1
- # syntax=docker/dockerfile:1
2
- FROM --platform=linux/amd64 node:20-bookworm-slim
3
-
4
- WORKDIR /work/ssh-client-wrapper
5
- RUN apt-get update && apt -y install ssh rsync bzip2 python3 g++ build-essential
6
- COPY ./ ./
7
- RUN npm install
@@ -1,7 +0,0 @@
1
- # syntax=docker/dockerfile:1
2
- FROM --platform=linux/amd64 node:20-bookworm-slim
3
-
4
- WORKDIR /work/ssh-client-wrapper
5
- RUN apt-get update && apt -y install ssh rsync bzip2 python3 g++ build-essential
6
- COPY ./ ./
7
- RUN npm install
package/Dockerfile-buster DELETED
@@ -1,7 +0,0 @@
1
- # syntax=docker/dockerfile:1
2
- FROM --platform=linux/amd64 node:20-bookworm-slim
3
-
4
- WORKDIR /work/ssh-client-wrapper
5
- RUN apt-get update && apt -y install ssh rsync bzip2 python3 g++ build-essential
6
- COPY ./ ./
7
- RUN npm install
package/compose.yml DELETED
@@ -1,36 +0,0 @@
1
- services:
2
- testbed:
3
- image: naoso5/test-sshd
4
- ports:
5
- - "4100:22"
6
- container_name: testbed
7
- tester:
8
- build:
9
- context: .
10
- dockerfile: Dockerfile
11
- environment:
12
- TEST_USER: testuser
13
- TEST_PW: passw0rd
14
- TEST_HOST: testbed
15
- working_dir: /work/ssh-client-wrapper
16
- command: bash -c "mkdir /root/.ssh 2>/dev/null; npm run mocha"
17
- tester-bullseye:
18
- build:
19
- context: .
20
- dockerfile: Dockerfile-bullseye
21
- environment:
22
- TEST_USER: testuser
23
- TEST_PW: passw0rd
24
- TEST_HOST: testbed
25
- working_dir: /work/ssh-client-wrapper
26
- command: bash -c "mkdir /root/.ssh 2>/dev/null; npm run mocha"
27
- tester-buster:
28
- build:
29
- context: .
30
- dockerfile: Dockerfile-buster
31
- environment:
32
- TEST_USER: testuser
33
- TEST_PW: passw0rd
34
- TEST_HOST: testbed
35
- working_dir: /work/ssh-client-wrapper
36
- command: bash -c "mkdir /root/.ssh 2>/dev/null; npm run mocha"
package/eslint.config.mjs DELETED
@@ -1,181 +0,0 @@
1
- import js from "@eslint/js";
2
- import globals from "globals";
3
- import stylistic from "@stylistic/eslint-plugin";
4
- import jsdoc from "eslint-plugin-jsdoc";
5
- import eslintPluginChaiFriendly from "eslint-plugin-chai-friendly";
6
- import eslintPluginChaiExpect from "eslint-plugin-chai-expect";
7
-
8
- const styleRules = {
9
- "arrow-body-style": ["error", "always"],
10
- "@stylistic/arrow-spacing": [
11
- "error",
12
- {
13
- after: false,
14
- before: false
15
- }],
16
- "@stylistic/comma-dangle": [
17
- "error",
18
- "never"
19
- ],
20
- "@stylistic/spaced-comment": [
21
- "error",
22
- "never"
23
- ],
24
- "@stylistic/lines-around-comment": [
25
- "error",
26
- {
27
- beforeBlockComment: true,
28
- afterBlockComment: false,
29
- beforeLineComment: false,
30
- afterLineComment: false,
31
- allowBlockStart: true
32
- }
33
- ],
34
- "@stylistic/lines-between-class-members": [
35
- "error",
36
- "always",
37
- {
38
- exceptAfterSingleLine: true
39
- }
40
- ],
41
- "@stylistic/newline-per-chained-call": [
42
- "error",
43
- {
44
- ignoreChainWithDepth: 2
45
- }
46
- ],
47
- "@stylistic/padded-blocks": [
48
- "error",
49
- "never"
50
- ],
51
- "@stylistic/brace-style": [
52
- "error",
53
- "1tbs",
54
- {
55
- allowSingleLine: true
56
- }
57
- ],
58
- "@stylistic/padding-line-between-statements": [
59
- "error",
60
- {
61
- blankLine: "any",
62
- prev: [
63
- "const",
64
- "let",
65
- "var"
66
- ],
67
- next: "*"
68
- },
69
- {
70
- blankLine: "always",
71
- prev: "*",
72
- next: [
73
- "class",
74
- "do",
75
- "for",
76
- "function",
77
- "switch",
78
- "try",
79
- "while"
80
- ]
81
- },
82
- {
83
- blankLine: "any",
84
- prev: [
85
- "const",
86
- "let",
87
- "var",
88
- "for",
89
- "while",
90
- "do",
91
- "block-like",
92
- "multiline-block-like"
93
- ],
94
- next: [
95
- "block-like",
96
- "do",
97
- "for",
98
- "multiline-block-like",
99
- "switch",
100
- "try",
101
- "while"
102
- ]
103
- }
104
- ],
105
- "@stylistic/quotes": [
106
- "error",
107
- "double"
108
- ],
109
- "@stylistic/semi": [
110
- "error",
111
- "always"
112
- ]
113
- };
114
-
115
- const jsdocRules = {
116
- "jsdoc/require-jsdoc": ["warn", { enableFixer: false }],
117
- "jsdoc/require-param": ["warn", { enableFixer: false }],
118
- "jsdoc/require-param-description": "warn",
119
- "jsdoc/require-param-name": "warn",
120
- "jsdoc/require-param-type": "warn",
121
- "jsdoc/require-hyphen-before-param-description": [
122
- "warn",
123
- "always"
124
- ],
125
- "jsdoc/require-returns": "warn",
126
- "jsdoc/require-returns-check": "error"
127
- };
128
-
129
- export default [
130
- // Global configuration
131
- {
132
- ignores: ["coverage-report.lcov", "node_modules/"]
133
- },
134
- js.configs.recommended,
135
- {
136
- plugins: {
137
- "@stylistic": stylistic
138
- }
139
- },
140
- // Lib configuration
141
- {
142
- files: ["lib/**/*.js"],
143
- plugins: {
144
- jsdoc
145
- },
146
- languageOptions: {
147
- ecmaVersion: "latest",
148
- sourceType: "module",
149
- globals: {
150
- ...globals.es2021,
151
- ...globals.node
152
- }
153
- },
154
- rules: {
155
- ...styleRules,
156
- ...jsdocRules
157
- }
158
- },
159
- // Test configuration
160
- {
161
- files: ["test/**/*.js"],
162
- plugins: {
163
- "chai-friendly": eslintPluginChaiFriendly,
164
- "chai-expect": eslintPluginChaiExpect
165
- },
166
- languageOptions: {
167
- ecmaVersion: "latest",
168
- sourceType: "module",
169
- globals: {
170
- ...globals.es2021,
171
- ...globals.node,
172
- ...globals.mocha
173
- }
174
- },
175
- rules: {
176
- ...styleRules,
177
- "no-unused-expressions": "off",
178
- "chai-friendly/no-unused-expressions": "error"
179
- }
180
- }
181
- ];