Skip to content

Commit

Permalink
feat(parse-function): v6, resolves #65
Browse files Browse the repository at this point in the history
BREAKING CHANGE: exports named `parseFunction`,

For more see #65 (comment)

Signed-off-by: Charlike Mike Reagent <[email protected]>
  • Loading branch information
tunnckoCore committed Oct 24, 2019
1 parent 95d6666 commit f56e21f
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 578 deletions.
Empty file added @types/.gitkeep
Empty file.
41 changes: 41 additions & 0 deletions packages/parse-function/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { parse as acornParse } from 'acorn';
import { parseFunction } from '.';

// `node` is an AST Node
function bobbyPlugin(node, result) {
const bobby = 'bobby';

return { ...result, bobby };
}

function barryPlugin(node, result) {
return { ...result, barry: 'barry barry' };
}

const result = parseFunction(bobbyPlugin.toString(), {
parse: acornParse,
plugins: [bobbyPlugin, barryPlugin], // supports array of plugins
parserOptions: {},
});

console.log(result);

/* {
name: 'bobbyPlugin',
body: "\n const bobby = 'bobby';\n\n return { ...result, bobby };\n",
args: [ 'node', 'result' ],
params: 'node, result',
defaults: { node: undefined, result: undefined },
value: '(function bobbyPlugin(node, result) {\n const ' +
"bobby = 'bobby';\n\n return { ...result, bobby };\n" +
'})',
isValid: true,
isArrow: false,
isAsync: false,
isNamed: true,
isAnonymous: false,
isGenerator: false,
isExpression: false,
bobby: 'bobby',
barry: 'barry barry'
} */
34 changes: 34 additions & 0 deletions packages/parse-function/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ParserOptions } from '@babel/parser';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type FnType = (...args: any) => any;

export type Input = FnType | string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Plugin = (node: any, result: Result) => Result | undefined;
export type Plugins = Plugin | Array<Plugin>;

export interface Options {
parse?(input: string, options?: ParserOptions): import('@babel/types').File;
parserOptions?: ParserOptions;
plugins?: Plugins;
}

export interface Result {
name: string | null;
body: string;
args: Array<string>;
params: string;
defaults: { [key: string]: string | undefined };
value: string;
isValid: boolean;
isArrow: boolean;
isAsync: boolean;
isNamed: boolean;
isAnonymous: boolean;
isGenerator: boolean;
isExpression: boolean;
}


export function parseFunction(code: Input, options?: Options): Result
5 changes: 3 additions & 2 deletions packages/parse-function/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"typings": "index.d.ts",
"files": [
"dist"
"dist",
"index.d.ts"
],
"keywords": [
"args",
Expand Down
244 changes: 27 additions & 217 deletions packages/parse-function/src/index.js
Original file line number Diff line number Diff line change
@@ -1,228 +1,38 @@
/**
* Utilities
*/
/* eslint-disable node/file-extension-in-import, import/extensions */

import utils from './utils';
import arrayify from 'arrify';
import { parse as babelParse } from '@babel/parser';

/**
* Core plugins
*/
import { setDefaults, getNode } from './utils.js';
import basePlugin from './plugins/initial.js';

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

/**
* > Initializes with optional `opts` object which is passed directly
* to the desired parser and returns an object
* with `.use` and `.parse` methods. The default parse which
* is used is [babylon][]'s `.parseExpression` method from `v7`.
*
* ```js
* const parseFunction = require('parse-function')
*
* const app = parseFunction({
* ecmaVersion: 2017
* })
*
* const fixtureFn = (a, b, c) => {
* a = b + c
* return a + 2
* }
*
* const result = app.parse(fixtureFn)
* console.log(result)
*
* // see more
* console.log(result.name) // => null
* console.log(result.isNamed) // => false
* console.log(result.isArrow) // => true
* console.log(result.isAnonymous) // => true
*
* // array of names of the arguments
* console.log(result.args) // => ['a', 'b', 'c']
*
* // comma-separated names of the arguments
* console.log(result.params) // => 'a, b, c'
* ```
*
* @param {Object} `opts` optional, merged with options passed to `.parse` method
* @return {Object} `app` object with `.use` and `.parse` methods
* @name parseFunction
* @api public
*/
export default function parseFunction(opts = {}) {
const plugins = [];
const app = {
/**
* > Parse a given `code` and returns a `result` object
* with useful properties - such as `name`, `body` and `args`.
* By default it uses Babylon parser, but you can switch it by
* passing `options.parse` - for example `options.parse: acorn.parse`.
* In the below example will show how to use `acorn` parser, instead
* of the default one.
*
* ```js
* const acorn = require('acorn')
* const parseFn = require('parse-function')
* const app = parseFn()
*
* const fn = function foo (bar, baz) { return bar * baz }
* const result = app.parse(fn, {
* parse: acorn.parse,
* ecmaVersion: 2017
* })
*
* console.log(result.name) // => 'foo'
* console.log(result.args) // => ['bar', 'baz']
* console.log(result.body) // => ' return bar * baz '
* console.log(result.isNamed) // => true
* console.log(result.isArrow) // => false
* console.log(result.isAnonymous) // => false
* console.log(result.isGenerator) // => false
* ```
*
* @param {Function|String} `code` any kind of function or string to be parsed
* @param {Object} `options` directly passed to the parser - babylon, acorn, espree
* @param {Function} `options.parse` by default `babylon.parseExpression`,
* all `options` are passed as second argument
* to that provided function
* @return {Object} `result` see [result section](#result) for more info
* @name .parse
* @api public
*/
parse(code, options) {
const result = utils.setDefaults(code);
if (!result.isValid) {
return result;
}

if (!result.isValid) {
return result;
}
const isFunction = result.value.startsWith('function');
const isAsyncFn = result.value.startsWith('async function');
const isAsync = result.value.startsWith('async');
const isArrow = result.value.includes('=>');
const isAsyncArrow = isAsync && isArrow;

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

const isFunction = result.value.startsWith('function');
const isAsyncFn = result.value.startsWith('async function');
const isAsync = result.value.startsWith('async');
const isArrow = result.value.includes('=>');
const isAsyncArrow = isAsync && isArrow;
if (!(isFunction || isAsyncFn || isAsyncArrow) && isMethod) {
result.value = `{ ${result.value} }`;
}

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

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

const node = utils.getNode(result, mergedOptions);
return plugins.reduce((res, fn) => fn(node, res) || res, result);
},

/**
* > Add a plugin `fn` function for extending the API or working on the
* AST nodes. The `fn` is immediately invoked and passed
* with `app` argument which is instance of `parseFunction()` call.
* That `fn` may return another function that
* accepts `(node, result)` signature, where `node` is an AST node
* and `result` is an object which will be returned [result](#result)
* from the `.parse` method. This retuned function is called on each
* node only when `.parse` method is called.
*
* _See [Plugins Architecture](#plugins-architecture) section._
*
* ```js
* // plugin extending the `app`
* app.use((app) => {
* app.define(app, 'hello', (place) => `Hello ${place}!`)
* })
*
* const hi = app.hello('World')
* console.log(hi) // => 'Hello World!'
*
* // or plugin that works on AST nodes
* app.use((app) => (node, result) => {
* if (node.type === 'ArrowFunctionExpression') {
* result.thatIsArrow = true
* }
* return result
* })
*
* const result = app.parse((a, b) => (a + b + 123))
* console.log(result.name) // => null
* console.log(result.isArrow) // => true
* console.log(result.thatIsArrow) // => true
*
* const result = app.parse(function foo () { return 123 })
* console.log(result.name) // => 'foo'
* console.log(result.isArrow) // => false
* console.log(result.thatIsArrow) // => undefined
* ```
*
* @param {Function} `fn` plugin to be called
* @return {Object} `app` instance for chaining
* @name .use
* @api public
*/
use(fn) {
const ret = fn(app);
if (typeof ret === 'function') {
plugins.push(ret);
}
return app;
},

/**
* > Define a non-enumerable property on an object. Just
* a convenience mirror of the [define-property][] library,
* so check out its docs. Useful to be used in plugins.
*
* ```js
* const parseFunction = require('parse-function')
* const app = parseFunction()
*
* // use it like `define-property` lib
* const obj = {}
* app.define(obj, 'hi', 'world')
* console.log(obj) // => { hi: 'world' }
*
* // or define a custom plugin that adds `.foo` property
* // to the end result, returned from `app.parse`
* app.use((app) => {
* return (node, result) => {
* // this function is called
* // only when `.parse` is called
*
* app.define(result, 'foo', 123)
*
* return result
* }
* })
*
* // fixture function to be parsed
* const asyncFn = async (qux) => {
* const bar = await Promise.resolve(qux)
* return bar
* }
*
* const result = app.parse(asyncFn)
*
* console.log(result.name) // => null
* console.log(result.foo) // => 123
* console.log(result.args) // => ['qux']
*
* console.log(result.isAsync) // => true
* console.log(result.isArrow) // => true
* console.log(result.isNamed) // => false
* console.log(result.isAnonymous) // => true
* ```
*
* @param {Object} `obj` the object on which to define the property
* @param {String} `prop` the name of the property to be defined or modified
* @param {Any} `val` the descriptor for the property being defined or modified
* @return {Object} `obj` the passed object, but modified
* @name .define
* @api public
*/
define: utils.define,
};

app.use(initial);

return app;
return pluginResult;
}, result);
}
20 changes: 9 additions & 11 deletions packages/parse-function/src/plugins/body.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
/* eslint-disable no-param-reassign, unicorn/consistent-function-scoping */

/**
* > Micro plugin to get the raw body, without the
* surrounding curly braces. It also preserves
* the whitespaces and newlines - they are original.
*
* @param {Object} node
* @param {Object} result
* @return {Object} result
* @param node
* @param result
* @return result
* @private
*/
export default () => (node, result) => {
result.body = result.value.slice(node.body.start, node.body.end);
export default (node, result) => {
let body = result.value.slice(node.body.start, node.body.end);

const openCurly = result.body.charCodeAt(0) === 123;
const closeCurly = result.body.charCodeAt(result.body.length - 1) === 125;
const openCurly = body.charCodeAt(0) === 123;
const closeCurly = body.charCodeAt(body.length - 1) === 125;

if (openCurly && closeCurly) {
result.body = result.body.slice(1, -1);
body = body.slice(1, -1);
}

return result;
return { ...result, body };
};
Loading

0 comments on commit f56e21f

Please sign in to comment.