fx 20.0.1 → 24.1.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
@@ -1,108 +1,60 @@
1
- <p align="center"><a href="http://fx.wtf"><img src="https://medv.io/assets/fx-logo.png" height="100" alt="fx logo"></a></p>
2
- <p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
1
+ # fx
3
2
 
4
- _* Function eXecution_
3
+ The JavaScript version of the **fx**. Short for _Function eXecution_ or _f(x)_.
5
4
 
6
- [![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
7
- [![Npm Version](https://img.shields.io/npm/v/fx.svg)](https://www.npmjs.com/package/fx)
8
- [![Brew Version](https://img.shields.io/homebrew/v/fx.svg)](https://formulae.brew.sh/formula/fx)
9
-
10
- Command-line JSON processing tool
11
-
12
- ## Features
13
-
14
- * Easy to use
15
- * Standalone binary
16
- * Interactive mode 🎉
17
- * Streaming support 🌊
18
-
19
- ## Install
20
-
21
- ```bash
22
- npm install -g fx
23
- ```
24
- Or via Homebrew
25
- ```bash
26
- brew install fx
27
- ```
28
- Or download standalone binary from [releases](https://github.com/antonmedv/fx/releases)
29
- ```bash
30
- bash <( curl https://fx.wtf )
5
+ ```sh
6
+ npm i -g fx
31
7
  ```
32
8
 
33
- ## Usage
9
+ Or use **npx**:
34
10
 
35
- Start [interactive mode](https://github.com/antonmedv/fx/blob/master/DOCS.md#interactive-mode) without passing any arguments.
36
- ```bash
37
- $ curl ... | fx
11
+ ```sh
12
+ cat file.json | npx fx .param
38
13
  ```
39
14
 
40
- Or by passing filename as first argument.
41
- ```bash
42
- $ fx data.json
43
- ```
15
+ Or use **node**:
44
16
 
45
- Pass a few JSON files.
46
- ```bash
47
- cat foo.json bar.json baz.json | fx .message
17
+ ```sh
18
+ cat file.json | node <(curl -s https://fx.wtf) .param
48
19
  ```
49
20
 
50
- Use full power of JavaScript.
51
- ```bash
52
- $ curl ... | fx '.filter(x => x.startsWith("a"))'
53
- ```
21
+ Or use **deno**:
54
22
 
55
- Access all lodash (or ramda, etc) methods by using [.fxrc](https://github.com/antonmedv/fx/blob/master/DOCS.md#using-fxrc) file.
56
- ```bash
57
- $ curl ... | fx '_.groupBy("commit.committer.name")' '_.mapValues(_.size)'
23
+ ```sh
24
+ cat file.json | deno run https://fx.wtf .param
58
25
  ```
59
26
 
60
- Update JSON using spread operator.
61
- ```bash
62
- $ echo '{"count": 0}' | fx '{...this, count: 1}'
63
- {
64
- "count": 1
65
- }
66
- ```
27
+ ## Usage
67
28
 
68
- Extract values from maps.
69
- ```bash
70
- $ fx commits.json | fx .[].author.name
71
- ```
29
+ Fx treats arguments as JavaScript functions. Fx passes the input data to the first
30
+ function and then passes the result of the first function to the second function
31
+ and so on.
72
32
 
73
- Print formatted JSON to stdout.
74
- ```bash
75
- $ curl ... | fx .
33
+ ```sh
34
+ echo '{"name": "world"}' | fx 'x => x.name' 'x => `Hello, ${x}!`'
76
35
  ```
77
36
 
78
- Pipe JSON logs stream into fx.
79
- ```bash
80
- $ kubectl logs ... -f | fx .message
81
- ```
37
+ Use `this` to access the input data. Use `.` at the start of the expression to
38
+ access the input data without a `x => x` part.
82
39
 
83
- And try this:
84
- ```bash
85
- $ fx --life
40
+ ```sh
41
+ echo '{"name": "world"}' | fx '.name' '`Hello, ${this}!`'
86
42
  ```
87
43
 
88
- ## Documentation
89
-
90
- See full [documentation](https://github.com/antonmedv/fx/blob/master/DOCS.md).
44
+ Fx has a shortcut for the map function. Fox example `.map(x => x.commit.message)`
45
+ can be written without leading dot and without `x =>` parts.
91
46
 
92
- ## Links
93
-
94
- * [Discover how to use fx effectively](https://medium.com/@antonmedv/discover-how-to-use-fx-effectively-668845d2a4ea)
95
-
96
- ## Related
47
+ ```sh
48
+ curl https://api.github.com/repos/antonmedv/fx/commits | fx 'map(x.commit.message)'
49
+ ```
97
50
 
98
- * [gofx](https://github.com/antonmedv/gofx) – fx-like JSON tool (*go*)
99
- * [eat](https://github.com/antonmedv/eat) – converts anything into JSON
100
- * [ymlx](https://github.com/matthewadams/ymlx) – fx-like YAML cli processor
101
- * [fx-completion](https://github.com/antonmedv/fx-completion) – bash completion for fx
102
- * [fx-theme-monokai](https://github.com/antonmedv/fx-theme-monokai) – monokai theme
103
- * [fx-theme-night](https://github.com/antonmedv/fx-theme-night) – night theme
51
+ Fx has a special syntax for the flatMap function. Fox example,
52
+ `.flatMap(x => x.labels.flatMap(x => x.name))` can be written in the next way.
104
53
 
54
+ ```sh
55
+ curl https://api.github.com/repos/kubernetes/kubernetes/issues | fx '.[].labels[].name'
56
+ ```
105
57
 
106
58
  ## License
107
59
 
108
- [MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)
60
+ [MIT](../LICENSE)
package/index.js CHANGED
@@ -1,174 +1,102 @@
1
- #!/usr/bin/env node
2
- 'use strict'
3
- const os = require('os')
4
- const fs = require('fs')
5
- const path = require('path')
6
- const JSON = require('./json')
7
- const std = require('./std')
8
-
9
- try {
10
- require(path.join(os.homedir(), '.fxrc')) // Should be required before config.js usage.
11
- } catch (err) {
12
- if (err.code !== 'MODULE_NOT_FOUND') {
13
- throw err
14
- }
15
- }
16
-
17
- const print = require('./print')
18
- const reduce = require('./reduce')
19
- const stream = require('./stream')
20
-
21
- const usage = `
22
- Usage
23
- $ fx [code ...]
24
-
25
- Examples
26
- $ echo '{"key": "value"}' | fx 'x => x.key'
27
- value
28
-
29
- $ echo '{"key": "value"}' | fx .key
30
- value
31
-
32
- $ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
33
- [2, 4, 6]
34
-
35
- $ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
36
- two
37
-
38
- $ echo '{"count": 0}' | fx '{...this, count: 1}'
39
- {"count": 1}
40
-
41
- $ echo '{"foo": 1, "bar": 2}' | fx ?
42
- ["foo", "bar"]
43
-
44
- `
45
-
46
- const {stdin, stdout, stderr} = process
47
- const args = process.argv.slice(2)
48
-
49
-
50
- void function main() {
51
- stdin.setEncoding('utf8')
1
+ void async function main() {
2
+ const process = await import('node:process')
3
+ let input = ''
4
+ process.stdin.setEncoding('utf8')
5
+ for await (const chunk of process.stdin)
6
+ input += chunk
7
+ const args = process.argv.slice(2)
52
8
 
53
- if (stdin.isTTY) {
54
- handle('')
55
- return
9
+ let json
10
+ try {
11
+ json = JSON.parse(input)
12
+ } catch (err) {
13
+ try {
14
+ json = eval(`(${input})`)
15
+ } catch (_) {
16
+ process.stderr.write(`Invalid JSON: ${err.message}\n`)
17
+ process.exit(1)
18
+ }
56
19
  }
57
20
 
58
- const reader = stream(stdin, apply)
21
+ let i, code, output = json
22
+ for ([i, code] of args.entries()) try {
23
+ output = transform(output, code)
24
+ } catch (err) {
25
+ printErr(err)
26
+ process.exit(1)
27
+ }
59
28
 
60
- stdin.on('readable', reader.read)
61
- stdin.on('end', () => {
62
- if (!reader.isStream()) {
63
- handle(reader.value())
64
- }
65
- })
29
+ if (typeof output === 'undefined')
30
+ process.stderr.write('undefined\n')
31
+ else if (typeof output === 'string')
32
+ console.log(output)
33
+ else
34
+ console.log(JSON.stringify(output, null, 2))
35
+
36
+ function printErr(err) {
37
+ let pre = args.slice(0, i).join(' ')
38
+ let post = args.slice(i + 1).join(' ')
39
+ if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
40
+ if (post.length > 20) post = post.substring(0, 20) + '...'
41
+ process.stderr.write(
42
+ `\n ${pre} ${code} ${post}\n` +
43
+ ` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
44
+ `\n${err.stack || err}\n`
45
+ )
46
+ }
66
47
  }()
67
48
 
49
+ function transform(json, code) {
50
+ if ('.' === code)
51
+ return json
68
52
 
69
- function handle(input) {
70
- let filename = 'fx'
71
-
72
- if (input === '') {
73
- if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
74
- stderr.write(usage)
75
- process.exit(2)
76
- }
77
- if (args.length === 1 && (args[0] === '-v' || args[0] === '--version')) {
78
- stderr.write(require('./package.json').version + '\n')
79
- process.exit(2)
80
- }
81
- if (args.length === 1 && args[0] === '--life') {
82
- require('./bang')
83
- return
84
- }
85
-
86
- input = fs.readFileSync(args[0]).toString('utf8')
87
- filename = path.basename(args[0])
88
- global.FX_FILENAME = filename
89
- args.shift()
90
- }
53
+ if ('?' === code)
54
+ return Object.keys(json)
91
55
 
92
- let json
93
- try {
94
- json = JSON.parse(input)
95
- } catch (e) {
96
- printError(e, input)
97
- process.exit(1)
98
- }
56
+ if (/^(\.\w*)+\[]/.test(code))
57
+ return eval(`(function () {
58
+ return (${fold(code.split('[]'))})(this)
59
+ })`).call(json)
99
60
 
100
- if (args.length === 0 && stdout.isTTY) {
101
- require('./fx')(filename, json)
102
- return
103
- }
61
+ if (/^\.\[/.test(code))
62
+ return eval(`(function () {
63
+ return this${code.substring(1)}
64
+ })`).call(json)
104
65
 
105
- apply(json)
106
- }
66
+ if (/^\./.test(code))
67
+ return eval(`(function () {
68
+ return this${code}
69
+ })`).call(json)
107
70
 
108
- function apply(json) {
109
- let output = json
71
+ if (/^map\(.+?\)$/.test(code))
72
+ return eval(`(function () {
73
+ return this.map(x => apply(x, ${code.substring(4, code.length - 1)}))
74
+ })`).call(json)
110
75
 
111
- for (let [i, code] of args.entries()) {
112
- try {
113
- output = reduce(output, code)
114
- } catch (e) {
115
- if (e === std.skip) {
116
- return
117
- }
118
- stderr.write(`${snippet(i, code)}\n${e.stack || e}\n`)
119
- process.exit(1)
120
- }
121
- }
76
+ const fn = eval(`(function () {
77
+ return ${code}
78
+ })`).call(json)
122
79
 
123
- if (typeof output === 'undefined') {
124
- stderr.write('undefined\n')
125
- } else if (typeof output === 'string') {
126
- console.log(output)
127
- } else {
128
- const [text] = print(output)
129
- console.log(text)
130
- }
80
+ return apply(json, fn)
131
81
  }
132
82
 
133
- function snippet(i, code) {
134
- let pre = args.slice(0, i).join(' ')
135
- let post = args.slice(i + 1).join(' ')
136
- if (pre.length > 20) {
137
- pre = '...' + pre.substring(pre.length - 20)
138
- }
139
- if (post.length > 20) {
140
- post = post.substring(0, 20) + '...'
141
- }
142
- const chalk = require('chalk')
143
- return `\n ${pre} ${chalk.red.underline(code)} ${post}\n`
83
+ function apply(json, fn) {
84
+ if (typeof fn === 'function') return fn(json)
85
+ return fn
144
86
  }
145
87
 
146
- function printError(e, input) {
147
- if (e.char) {
148
- let lineNumber = 1, start = e.char - 70, end = e.char + 50
149
- if (start < 0) start = 0
150
- if (end > input.length) end = input.length
151
-
152
- for (let i = 0; i < input.length && i < start; i++) {
153
- if (input[i] === '\n') lineNumber++
154
- }
155
-
156
- let lines = input
157
- .substring(start, end)
158
- .split('\n')
88
+ function fold(s) {
89
+ if (s.length === 1)
90
+ return 'x => x' + s[0]
91
+ let obj = s.shift()
92
+ obj = obj === '.' ? 'x' : 'x' + obj
93
+ return `x => Object.values(${obj}).flatMap(${fold(s)})`
94
+ }
159
95
 
160
- if (lines.length > 1) {
161
- lines = lines.slice(1)
162
- lineNumber++
163
- }
96
+ function uniq(array) {
97
+ return [...new Set(array)]
98
+ }
164
99
 
165
- const chalk = require('chalk')
166
- process.stderr.write(`\n`)
167
- for (let line of lines) {
168
- process.stderr.write(` ${chalk.yellow(lineNumber)} ${line}\n`)
169
- lineNumber++
170
- }
171
- process.stderr.write(`\n`)
172
- }
173
- process.stderr.write(e.toString() + '\n')
100
+ function sort(array) {
101
+ return array.sort()
174
102
  }
package/package.json CHANGED
@@ -1,53 +1,18 @@
1
1
  {
2
2
  "name": "fx",
3
- "version": "20.0.1",
4
- "description": "Command-line JSON viewer",
5
- "repository": "antonmedv/fx",
6
- "homepage": "https://fx.wtf",
7
- "author": "Anton Medvedev <anton@medv.io>",
8
- "license": "MIT",
3
+ "version": "24.1.0",
9
4
  "bin": {
10
5
  "fx": "index.js"
11
6
  },
12
7
  "files": [
13
- "*.js"
8
+ "index.js"
14
9
  ],
15
10
  "scripts": {
16
- "test": "ava",
17
- "build": "pkg . --out-path dist",
18
- "zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
19
- "release": "release-it --github.release --github.assets=dist/*.zip",
20
- "brew:release": "brew bump-formula-pr --url=https://registry.npmjs.org/fx/-/fx-`fx package.json .version`.tgz --no-audit fx"
21
- },
22
- "release-it": {
23
- "hooks": {
24
- "after:bump": "rm -rf ./dist && npm run build && npm run zip"
25
- }
26
- },
27
- "keywords": [
28
- "json",
29
- "viewer",
30
- "cli",
31
- "terminal",
32
- "term",
33
- "console",
34
- "ascii",
35
- "unicode",
36
- "blessed"
37
- ],
38
- "engines": {
39
- "node": ">=8"
40
- },
41
- "dependencies": {
42
- "@medv/blessed": "^2.0.1",
43
- "chalk": "^4.1.0",
44
- "indent-string": "^4.0.0",
45
- "lossless-json": "^1.0.4",
46
- "string-width": "^4.2.0"
11
+ "test": "node test.js"
47
12
  },
48
- "devDependencies": {
49
- "ava": "^3.12.1",
50
- "pkg": "^4.4.9",
51
- "release-it": "^14.0.1"
52
- }
13
+ "repository": "antonmedv/fx",
14
+ "homepage": "https://fx.wtf",
15
+ "description": "Command-line JSON viewer",
16
+ "author": "Anton Medvedev <anton@medv.io>",
17
+ "license": "MIT"
53
18
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2019 Anton Medvedev
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/bang.js DELETED
@@ -1,79 +0,0 @@
1
- 'use strict'
2
- // http://bit.ly/fx--life
3
-
4
- const
5
- run = f => setInterval(f, 16),
6
- p = (s) => process.stdout.write(s),
7
- esc = (...x) => x.map(i => p('\u001B[' + i)),
8
- [upper, lower, full] = ['\u2580', '\u2584', '\u2588'],
9
- {columns, rows} = process.stdout,
10
- [w, h] = [columns, rows * 2],
11
- a = Math.floor(w / 2) - 6, b = Math.floor(h / 2) - 7
12
-
13
- let $ = Array(w * h).fill(false)
14
- if (Date.now() % 3 === 0) {
15
- $[1 + 5 * w] = $[1 + 6 * w] = $[2 + 5 * w] = $[2 + 6 * w] = $[12 + 5 * w] = $[12 + 6 * w] = $[12 + 7 * w] = $[13 + 4 * w] = $[13 + 8 * w] = $[14 + 3 * w] = $[14 + 9 * w] = $[15 + 4 * w] = $[15 + 8 * w] = $[16 + 5 * w] = $[16 + 6 * w] = $[16 + 7 * w] = $[17 + 5 * w] = $[17 + 6 * w] = $[17 + 7 * w] = $[22 + 3 * w] = $[22 + 4 * w] = $[22 + 5 * w] = $[23 + 2 * w] = $[23 + 3 * w] = $[23 + 5 * w] = $[23 + 6 * w] = $[24 + 2 * w] = $[24 + 3 * w] = $[24 + 5 * w] = $[24 + 6 * w] = $[25 + 2 * w] = $[25 + 3 * w] = $[25 + 4 * w] = $[25 + 5 * w] = $[25 + 6 * w] = $[26 + 1 * w] = $[26 + 2 * w] = $[26 + 6 * w] = $[26 + 7 * w] = $[35 + 3 * w] = $[35 + 4 * w] = $[36 + 3 * w] = $[36 + 4 * w] = true
16
- } else if (Date.now() % 3 === 1) {
17
- for (let i = 0; i < $.length; i-=-1)
18
- if (Math.random() < 0.16) $[i] = true
19
- } else {
20
- $[a + 1 + (2 + b) * w] = $[a + 2 + (1 + b) * w] = $[a + 2 + (3 + b) * w] = $[a + 3 + (2 + b) * w] = $[a + 5 + (15 + b) * w] = $[a + 6 + (13 + b) * w] = $[a + 6 + (15 + b) * w] = $[a + 7 + (12 + b) * w] = $[a + 7 + (13 + b) * w] = $[a + 7 + (15 + b) * w] = $[a + 9 + (11 + b) * w] = $[a + 9 + (12 + b) * w] = $[a + 9 + (13 + b) * w] = true
21
- }
22
-
23
- function at(i, j) {
24
- if (i < 0) i = h - 1
25
- if (i >= h) i = 0
26
- if (j < 0) j = w - 1
27
- if (j >= w) j = 0
28
- return $[i * w + j]
29
- }
30
-
31
- function neighbors(i, j) {
32
- let c = 0
33
- at(i - 1, j - 1) && c++
34
- at(i - 1, j) && c++
35
- at(i - 1, j + 1) && c++
36
- at(i, j - 1) && c++
37
- at(i, j + 1) && c++
38
- at(i + 1, j - 1) && c++
39
- at(i + 1, j) && c++
40
- at(i + 1, j + 1) && c++
41
- return c
42
- }
43
-
44
- run(() => {
45
- esc('H')
46
-
47
- let gen = Array(w * h).fill(false)
48
- for (let i = 0; i < h; i-=-1) {
49
- for (let j = 0; j < w; j-=-1) {
50
- const n = neighbors(i, j)
51
- const z = i * w + j
52
- if ($[z]) {
53
- if (n < 2) gen[z] = false
54
- if (n === 2 || n === 3) gen[z] = true
55
- if (n > 3) gen[z] = false
56
- } else {
57
- if (n === 3) gen[z] = true
58
- }
59
- }
60
- }
61
- $ = gen
62
-
63
- for (let i = 0; i < rows; i-=-1) {
64
- for (let j = 0; j < columns; j-=-1) {
65
- if ($[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(full)
66
- else if ($[i * 2 * w + j] && !$[(i * 2 + 1) * w + j]) p(upper)
67
- else if (!$[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(lower)
68
- else p(' ')
69
- }
70
- if (i !== rows - 1) p('\n')
71
- }
72
- })
73
-
74
- esc('2J', '?25l')
75
-
76
- process.on('SIGINT', () => {
77
- esc('?25h')
78
- process.exit(2)
79
- })
package/config.js DELETED
@@ -1,26 +0,0 @@
1
- 'use strict'
2
- const chalk = require('chalk')
3
- const noop = x => x
4
- const list = {
5
- fg: 'black',
6
- bg: 'cyan',
7
- selected: {
8
- bg: 'magenta'
9
- }
10
- }
11
-
12
- module.exports = {
13
- space: global.FX_STYLE_SPACE || 2,
14
- null: global.FX_STYLE_NULL || chalk.grey.bold,
15
- number: global.FX_STYLE_NUMBER || chalk.cyan.bold,
16
- boolean: global.FX_STYLE_BOOLEAN || chalk.yellow.bold,
17
- string: global.FX_STYLE_STRING || chalk.green.bold,
18
- key: global.FX_STYLE_KEY || chalk.blue.bold,
19
- bracket: global.FX_STYLE_BRACKET || noop,
20
- comma: global.FX_STYLE_COMMA || noop,
21
- colon: global.FX_STYLE_COLON || noop,
22
- list: global.FX_STYLE_LIST || list,
23
- highlight: global.FX_STYLE_HIGHLIGHT || chalk.black.bgYellow,
24
- highlightCurrent: global.FX_STYLE_HIGHLIGHT_CURRENT || chalk.inverse,
25
- statusBar: global.FX_STYLE_STATUS_BAR || chalk.inverse,
26
- }
package/find.js DELETED
@@ -1,35 +0,0 @@
1
- 'use strict'
2
-
3
- function* find(v, regex, path = []) {
4
- if (typeof v === 'undefined' || v === null) {
5
- return
6
- }
7
-
8
- if (Array.isArray(v)) {
9
- let i = 0
10
- for (let value of v) {
11
- yield* find(value, regex, path.concat(['[' + i++ + ']']))
12
- }
13
- return
14
- }
15
-
16
- if (typeof v === 'object' && v.constructor === Object) {
17
- const entries = Object.entries(v)
18
- for (let [key, value] of entries) {
19
- const nextPath = path.concat(['.' + key])
20
-
21
- if (regex.test(key)) {
22
- yield nextPath
23
- }
24
-
25
- yield* find(value, regex, nextPath)
26
- }
27
- return
28
- }
29
-
30
- if (regex.test(v)) {
31
- yield path
32
- }
33
- }
34
-
35
- module.exports = find