Skip to content

Commit f56e21f

Browse files
committed
feat(parse-function): v6, resolves #65
BREAKING CHANGE: exports named `parseFunction`, For more see #65 (comment) Signed-off-by: Charlike Mike Reagent <[email protected]>
1 parent 95d6666 commit f56e21f

File tree

12 files changed

+406
-578
lines changed

12 files changed

+406
-578
lines changed

@types/.gitkeep

Whitespace-only changes.

packages/parse-function/example.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { parse as acornParse } from 'acorn';
2+
import { parseFunction } from '.';
3+
4+
// `node` is an AST Node
5+
function bobbyPlugin(node, result) {
6+
const bobby = 'bobby';
7+
8+
return { ...result, bobby };
9+
}
10+
11+
function barryPlugin(node, result) {
12+
return { ...result, barry: 'barry barry' };
13+
}
14+
15+
const result = parseFunction(bobbyPlugin.toString(), {
16+
parse: acornParse,
17+
plugins: [bobbyPlugin, barryPlugin], // supports array of plugins
18+
parserOptions: {},
19+
});
20+
21+
console.log(result);
22+
23+
/* {
24+
name: 'bobbyPlugin',
25+
body: "\n const bobby = 'bobby';\n\n return { ...result, bobby };\n",
26+
args: [ 'node', 'result' ],
27+
params: 'node, result',
28+
defaults: { node: undefined, result: undefined },
29+
value: '(function bobbyPlugin(node, result) {\n const ' +
30+
"bobby = 'bobby';\n\n return { ...result, bobby };\n" +
31+
'})',
32+
isValid: true,
33+
isArrow: false,
34+
isAsync: false,
35+
isNamed: true,
36+
isAnonymous: false,
37+
isGenerator: false,
38+
isExpression: false,
39+
bobby: 'bobby',
40+
barry: 'barry barry'
41+
} */

packages/parse-function/index.d.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ParserOptions } from '@babel/parser';
2+
3+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4+
type FnType = (...args: any) => any;
5+
6+
export type Input = FnType | string;
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
export type Plugin = (node: any, result: Result) => Result | undefined;
9+
export type Plugins = Plugin | Array<Plugin>;
10+
11+
export interface Options {
12+
parse?(input: string, options?: ParserOptions): import('@babel/types').File;
13+
parserOptions?: ParserOptions;
14+
plugins?: Plugins;
15+
}
16+
17+
export interface Result {
18+
name: string | null;
19+
body: string;
20+
args: Array<string>;
21+
params: string;
22+
defaults: { [key: string]: string | undefined };
23+
value: string;
24+
isValid: boolean;
25+
isArrow: boolean;
26+
isAsync: boolean;
27+
isNamed: boolean;
28+
isAnonymous: boolean;
29+
isGenerator: boolean;
30+
isExpression: boolean;
31+
}
32+
33+
34+
export function parseFunction(code: Input, options?: Options): Result

packages/parse-function/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
},
1616
"main": "dist/cjs/index.js",
1717
"module": "dist/esm/index.js",
18-
"types": "dist/types/index.d.ts",
18+
"typings": "index.d.ts",
1919
"files": [
20-
"dist"
20+
"dist",
21+
"index.d.ts"
2122
],
2223
"keywords": [
2324
"args",

packages/parse-function/src/index.js

+27-217
Original file line numberDiff line numberDiff line change
@@ -1,228 +1,38 @@
1-
/**
2-
* Utilities
3-
*/
1+
/* eslint-disable node/file-extension-in-import, import/extensions */
42

5-
import utils from './utils';
3+
import arrayify from 'arrify';
4+
import { parse as babelParse } from '@babel/parser';
65

7-
/**
8-
* Core plugins
9-
*/
6+
import { setDefaults, getNode } from './utils.js';
7+
import basePlugin from './plugins/initial.js';
108

11-
import initial from './plugins/initial';
9+
// eslint-disable-next-line import/prefer-default-export
10+
export function parseFunction(code, options) {
11+
const opts = { parse: babelParse, ...options };
12+
const result = setDefaults(code);
1213

13-
/**
14-
* > Initializes with optional `opts` object which is passed directly
15-
* to the desired parser and returns an object
16-
* with `.use` and `.parse` methods. The default parse which
17-
* is used is [babylon][]'s `.parseExpression` method from `v7`.
18-
*
19-
* ```js
20-
* const parseFunction = require('parse-function')
21-
*
22-
* const app = parseFunction({
23-
* ecmaVersion: 2017
24-
* })
25-
*
26-
* const fixtureFn = (a, b, c) => {
27-
* a = b + c
28-
* return a + 2
29-
* }
30-
*
31-
* const result = app.parse(fixtureFn)
32-
* console.log(result)
33-
*
34-
* // see more
35-
* console.log(result.name) // => null
36-
* console.log(result.isNamed) // => false
37-
* console.log(result.isArrow) // => true
38-
* console.log(result.isAnonymous) // => true
39-
*
40-
* // array of names of the arguments
41-
* console.log(result.args) // => ['a', 'b', 'c']
42-
*
43-
* // comma-separated names of the arguments
44-
* console.log(result.params) // => 'a, b, c'
45-
* ```
46-
*
47-
* @param {Object} `opts` optional, merged with options passed to `.parse` method
48-
* @return {Object} `app` object with `.use` and `.parse` methods
49-
* @name parseFunction
50-
* @api public
51-
*/
52-
export default function parseFunction(opts = {}) {
53-
const plugins = [];
54-
const app = {
55-
/**
56-
* > Parse a given `code` and returns a `result` object
57-
* with useful properties - such as `name`, `body` and `args`.
58-
* By default it uses Babylon parser, but you can switch it by
59-
* passing `options.parse` - for example `options.parse: acorn.parse`.
60-
* In the below example will show how to use `acorn` parser, instead
61-
* of the default one.
62-
*
63-
* ```js
64-
* const acorn = require('acorn')
65-
* const parseFn = require('parse-function')
66-
* const app = parseFn()
67-
*
68-
* const fn = function foo (bar, baz) { return bar * baz }
69-
* const result = app.parse(fn, {
70-
* parse: acorn.parse,
71-
* ecmaVersion: 2017
72-
* })
73-
*
74-
* console.log(result.name) // => 'foo'
75-
* console.log(result.args) // => ['bar', 'baz']
76-
* console.log(result.body) // => ' return bar * baz '
77-
* console.log(result.isNamed) // => true
78-
* console.log(result.isArrow) // => false
79-
* console.log(result.isAnonymous) // => false
80-
* console.log(result.isGenerator) // => false
81-
* ```
82-
*
83-
* @param {Function|String} `code` any kind of function or string to be parsed
84-
* @param {Object} `options` directly passed to the parser - babylon, acorn, espree
85-
* @param {Function} `options.parse` by default `babylon.parseExpression`,
86-
* all `options` are passed as second argument
87-
* to that provided function
88-
* @return {Object} `result` see [result section](#result) for more info
89-
* @name .parse
90-
* @api public
91-
*/
92-
parse(code, options) {
93-
const result = utils.setDefaults(code);
14+
if (!result.isValid) {
15+
return result;
16+
}
9417

95-
if (!result.isValid) {
96-
return result;
97-
}
18+
const isFunction = result.value.startsWith('function');
19+
const isAsyncFn = result.value.startsWith('async function');
20+
const isAsync = result.value.startsWith('async');
21+
const isArrow = result.value.includes('=>');
22+
const isAsyncArrow = isAsync && isArrow;
9823

99-
const mergedOptions = { ...opts, ...options };
24+
const isMethod = /^\*?.+\([\s\S\w\W]*\)\s*\{/i.test(result.value);
10025

101-
const isFunction = result.value.startsWith('function');
102-
const isAsyncFn = result.value.startsWith('async function');
103-
const isAsync = result.value.startsWith('async');
104-
const isArrow = result.value.includes('=>');
105-
const isAsyncArrow = isAsync && isArrow;
26+
if (!(isFunction || isAsyncFn || isAsyncArrow) && isMethod) {
27+
result.value = `{ ${result.value} }`;
28+
}
10629

107-
const isMethod = /^\*?.+\([\s\S\w\W]*\)\s*\{/i.test(result.value);
30+
const node = getNode(result, opts);
31+
const plugins = arrayify(opts.plugins);
10832

109-
if (!(isFunction || isAsyncFn || isAsyncArrow) && isMethod) {
110-
result.value = `{ ${result.value} }`;
111-
}
33+
return [basePlugin, ...plugins].filter(Boolean).reduce((res, fn) => {
34+
const pluginResult = fn(node, { ...res }) || res;
11235

113-
const node = utils.getNode(result, mergedOptions);
114-
return plugins.reduce((res, fn) => fn(node, res) || res, result);
115-
},
116-
117-
/**
118-
* > Add a plugin `fn` function for extending the API or working on the
119-
* AST nodes. The `fn` is immediately invoked and passed
120-
* with `app` argument which is instance of `parseFunction()` call.
121-
* That `fn` may return another function that
122-
* accepts `(node, result)` signature, where `node` is an AST node
123-
* and `result` is an object which will be returned [result](#result)
124-
* from the `.parse` method. This retuned function is called on each
125-
* node only when `.parse` method is called.
126-
*
127-
* _See [Plugins Architecture](#plugins-architecture) section._
128-
*
129-
* ```js
130-
* // plugin extending the `app`
131-
* app.use((app) => {
132-
* app.define(app, 'hello', (place) => `Hello ${place}!`)
133-
* })
134-
*
135-
* const hi = app.hello('World')
136-
* console.log(hi) // => 'Hello World!'
137-
*
138-
* // or plugin that works on AST nodes
139-
* app.use((app) => (node, result) => {
140-
* if (node.type === 'ArrowFunctionExpression') {
141-
* result.thatIsArrow = true
142-
* }
143-
* return result
144-
* })
145-
*
146-
* const result = app.parse((a, b) => (a + b + 123))
147-
* console.log(result.name) // => null
148-
* console.log(result.isArrow) // => true
149-
* console.log(result.thatIsArrow) // => true
150-
*
151-
* const result = app.parse(function foo () { return 123 })
152-
* console.log(result.name) // => 'foo'
153-
* console.log(result.isArrow) // => false
154-
* console.log(result.thatIsArrow) // => undefined
155-
* ```
156-
*
157-
* @param {Function} `fn` plugin to be called
158-
* @return {Object} `app` instance for chaining
159-
* @name .use
160-
* @api public
161-
*/
162-
use(fn) {
163-
const ret = fn(app);
164-
if (typeof ret === 'function') {
165-
plugins.push(ret);
166-
}
167-
return app;
168-
},
169-
170-
/**
171-
* > Define a non-enumerable property on an object. Just
172-
* a convenience mirror of the [define-property][] library,
173-
* so check out its docs. Useful to be used in plugins.
174-
*
175-
* ```js
176-
* const parseFunction = require('parse-function')
177-
* const app = parseFunction()
178-
*
179-
* // use it like `define-property` lib
180-
* const obj = {}
181-
* app.define(obj, 'hi', 'world')
182-
* console.log(obj) // => { hi: 'world' }
183-
*
184-
* // or define a custom plugin that adds `.foo` property
185-
* // to the end result, returned from `app.parse`
186-
* app.use((app) => {
187-
* return (node, result) => {
188-
* // this function is called
189-
* // only when `.parse` is called
190-
*
191-
* app.define(result, 'foo', 123)
192-
*
193-
* return result
194-
* }
195-
* })
196-
*
197-
* // fixture function to be parsed
198-
* const asyncFn = async (qux) => {
199-
* const bar = await Promise.resolve(qux)
200-
* return bar
201-
* }
202-
*
203-
* const result = app.parse(asyncFn)
204-
*
205-
* console.log(result.name) // => null
206-
* console.log(result.foo) // => 123
207-
* console.log(result.args) // => ['qux']
208-
*
209-
* console.log(result.isAsync) // => true
210-
* console.log(result.isArrow) // => true
211-
* console.log(result.isNamed) // => false
212-
* console.log(result.isAnonymous) // => true
213-
* ```
214-
*
215-
* @param {Object} `obj` the object on which to define the property
216-
* @param {String} `prop` the name of the property to be defined or modified
217-
* @param {Any} `val` the descriptor for the property being defined or modified
218-
* @return {Object} `obj` the passed object, but modified
219-
* @name .define
220-
* @api public
221-
*/
222-
define: utils.define,
223-
};
224-
225-
app.use(initial);
226-
227-
return app;
36+
return pluginResult;
37+
}, result);
22838
}
+9-11
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1-
/* eslint-disable no-param-reassign, unicorn/consistent-function-scoping */
2-
31
/**
42
* > Micro plugin to get the raw body, without the
53
* surrounding curly braces. It also preserves
64
* the whitespaces and newlines - they are original.
75
*
8-
* @param {Object} node
9-
* @param {Object} result
10-
* @return {Object} result
6+
* @param node
7+
* @param result
8+
* @return result
119
* @private
1210
*/
13-
export default () => (node, result) => {
14-
result.body = result.value.slice(node.body.start, node.body.end);
11+
export default (node, result) => {
12+
let body = result.value.slice(node.body.start, node.body.end);
1513

16-
const openCurly = result.body.charCodeAt(0) === 123;
17-
const closeCurly = result.body.charCodeAt(result.body.length - 1) === 125;
14+
const openCurly = body.charCodeAt(0) === 123;
15+
const closeCurly = body.charCodeAt(body.length - 1) === 125;
1816

1917
if (openCurly && closeCurly) {
20-
result.body = result.body.slice(1, -1);
18+
body = body.slice(1, -1);
2119
}
2220

23-
return result;
21+
return { ...result, body };
2422
};

0 commit comments

Comments
 (0)