├── .babelrc ├── .gitignore ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── scripts ├── build.js ├── test.js └── testBuilds.js ├── src ├── compiler.js ├── context.js ├── index.js ├── modifiers.js ├── parser.js └── rawCompiler.js └── tests ├── a_modifiers.test.js ├── compiler.test.js └── context.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["es2016", "es2017", "stage-0"] 5 | }, 6 | "test": { 7 | "presets": ["es2016", "es2017", "stage-0"], 8 | "plugins": ["transform-es2015-modules-commonjs"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # reify cache for allowing import export in node 40 | .reify-cache 41 | 42 | # built code 43 | dist 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 RisingStack 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The compiler util 2 | 3 | This library is part of the [NX framework](http://nx-framework.com). 4 | 5 | The main purpose of this library is to allow the execution of strings as code in the 6 | context of an object. 7 | 8 | ## Installation 9 | 10 | ``` 11 | $ npm install @nx-js/compiler-util 12 | ``` 13 | 14 | ## Platform support 15 | 16 | - Node: 6 and above 17 | - Chrome: 49 and above (after browserified) 18 | - Firefox: 38 and above (after browserified) 19 | - Safari: 10 and above (after browserified) 20 | - Edge: 12 and above (after browserified) 21 | - Opera: 36 and above (after browserified) 22 | - IE is not supported 23 | 24 | ## Usage 25 | 26 | ```js 27 | const compiler = require('@nx-js/compiler-util') 28 | ``` 29 | 30 | ### Compiling code 31 | 32 | `compiler.compileCode(string)` creates a function from a string. The returned function takes 33 | an object as argument and executes the string as code in the context of the passed object. 34 | The string can be any valid JavaScript code. 35 | 36 | ```js 37 | const code = compiler.compileCode('return prop1 + prop2') 38 | const sum = code({prop1: 1, prop2: 2}) // sum is 3 39 | ``` 40 | 41 | #### Temporary variables 42 | 43 | The returned function also accepts a second object argument, that may contain temporary variables. 44 | Temporary variables are added to the context object while the code is executing. 45 | They are favored over the permanent context variables. 46 | 47 | ```js 48 | const code = compiler.compileCode('return prop1 + prop2') 49 | const context = {prop1: 1, prop2: 2} 50 | const temporary = {prop1: 2} 51 | const sum = code(context, temporary) // sum is 4, context is still {prop1: 1, prop2: 2} 52 | ``` 53 | 54 | #### Limiters 55 | 56 | Limiters are functions, which can defer or block code execution. Some popular limiters are debounce and throttle for example. Limiters can be registered by name with `compiler.limiter(name, function)` and used at the end of the code with the `&` symbol. 57 | 58 | ```js 59 | // next is the code or the next limiter 60 | compiler.limiter('delay', next => setTimeout(next, 1000)) 61 | 62 | const code = compiler.compileCode('console.log(message) & delay') 63 | const context = {message: 'Hello World'} 64 | code(context) // prints 'Hello World' to the console after a second 65 | ``` 66 | 67 | Limiters accept a context object, which can be used to share a context between executions of the code. It makes the creation of rate limiters - like throttle and debounce - straightforward. 68 | 69 | ```js 70 | compiler.limiter('debounce', debounce) 71 | 72 | function debounce (next, context) { 73 | clearTimeout(context.timer) 74 | context.timer = setTimeout(next, 200) 75 | } 76 | ``` 77 | 78 | After the context argument limiters accept any number of custom arguments. These can be passed after the limiter name in the code, separated by spaces. 79 | 80 | ```js 81 | compiler.limiter('delay', (next, context, amount) => setTimeout(next, amount)) 82 | 83 | const code = compiler.compileCode('console.log(message) & delay 2000') 84 | const code2 = compiler.compileCode('console.log(message) & delay amount') 85 | 86 | const context = {message: 'Hello World', amount: 3000} 87 | code(context) // prints 'Hello World' to the console after 2 seconds 88 | code2(context) // prints 'Hello World' to the console after 3 seconds 89 | ``` 90 | 91 | Multiple limiters can be piped with the `&` symbol. 92 | 93 | ```js 94 | const code = compiler.compileCode('console.log(message) & delay 1000 & throttle 100') 95 | 96 | // this logs 'Hello World' a second after you click the button 97 | // and it logs a message once per 100 milliseconds at most, excess messages are not logged 98 | button.addEventListener('code', () => code({message: 'Hello World'})) 99 | ``` 100 | 101 | You can find some commonly used limiters in [this repo](https://github.com/nx-js/limiters). 102 | 103 | ### Compiling expressions 104 | 105 | `compiler.compileExpression(string)` creates a function from a string. The returned function takes 106 | an object as argument and executes the string as an expression in the context of the passed object. 107 | It returns the result of the evaluated expression. The string can be any javascript expression 108 | that may come after a return statement. 109 | 110 | ```js 111 | const expression = compiler.compileExpression('prop1 || prop2') 112 | const result = expression({prop2: 'Hello'}) // result is 'Hello' 113 | ``` 114 | 115 | Expressions return undefined instead of throwing a TypeError on invalid property access. 116 | This allows lazy initialization of your data. 117 | 118 | ```js 119 | const expression = compiler.compileExpression('item.name') 120 | const context = {} 121 | 122 | let result = expression(context) // result is undefined, no error is thrown 123 | 124 | context.item = {name: 'item name'} 125 | result = expression(context) // result is 'item name' 126 | ``` 127 | 128 | #### Filters 129 | 130 | Filters are functions, which can filter and modify expression result. Some popular filters are upperCase and trim for example. Filters can be registered by name with `compiler.filter(name, function)` and used at the end of the expression with the `|` symbol. 131 | 132 | ```js 133 | // txt is the result of the expression 134 | compiler.filter('upperCase', txt => txt.toUpperCase()) 135 | 136 | const expr = compiler.compileExpression('message | upperCase') 137 | const context = {message: 'Hello World'} 138 | console.log(expr(context)) // prints 'HELLO WORLD' to the console 139 | ``` 140 | 141 | Filters accept any number of custom arguments. These can be passed after the filter name in the expression, separated by spaces. 142 | 143 | ```js 144 | compiler.filter('splice', (txt, start, end) => txt.splice(start, end)) 145 | 146 | const expr = compiler.compileExpression('message | splice 0 6') 147 | const context = {message: 'Hello World'} 148 | console.log(expr(context)) // prints 'Hello' to the console 149 | ``` 150 | 151 | Multiple filters can be piped with the `|` symbol. 152 | 153 | ```js 154 | const expr = compiler.compileExpression('message | splice 0 6 | upperCase') 155 | const context = {message: 'Hello World'} 156 | console.log(expr(context)) // prints 'HELLO' to the console 157 | ``` 158 | 159 | You can find some commonly used filters in [this repo](https://github.com/nx-js/filters). 160 | 161 | ### Handling globals 162 | 163 | `compiler.expose('String, String, ...')` exposes globals by name for the compiler. Non of the globals are exposed by default. 164 | 165 | ```js 166 | const code = compiler.compileCode('console.log(Math.round(num))') 167 | compiler.expose('console', 'Math') 168 | code({num: 1.8}) // logs 2 to the console 169 | ``` 170 | 171 | Context variables are always favored over global ones, when both are present with the same name. 172 | 173 | `compiler.hide(String, String, ...)` hides globals by name, while `compiler.hideAll()` hides all globals. 174 | 175 | ```js 176 | const code = compiler.compileCode('console.log(Math.round(num))') 177 | compiler.expose('console', 'Math') 178 | code({num: 1.8}) // logs 2 to the console 179 | compiler.hide('console', 'Math') 180 | code({num: 1.8}) // throws an error, console and Math are undefined 181 | ``` 182 | 183 | ## Alternative builds 184 | 185 | This library detects if you use ES or commonJS modules and serve the right format to you. The exposed bundles are transpiled to ES5 to support common tools - like UglifyJS. If you would like a finer control over the provided build, you can specify them in your imports. 186 | 187 | * `@nx-js/compiler-util/dist/es.es6.js` exposes an ES6 build with ES modules. 188 | * `@nx-js/compiler-util/dist/es.es5.js` exposes an ES5 build with ES modules. 189 | * `@nx-js/compiler-util/dist/cjs.es6.js` exposes an ES6 build with commonJS modules. 190 | * `@nx-js/compiler-util/dist/cjs.es5.js` exposes an ES5 build with commonJS modules. 191 | 192 | If you use a bundler, set up an alias for `@nx-js/compiler-util` to point to your desired build. You can learn how to do it with webpack [here](https://webpack.js.org/configuration/resolve/#resolve-alias) and with rollup [here](https://github.com/rollup/rollup-plugin-alias#usage). 193 | 194 | ## Contributions 195 | 196 | This library has the very specific purpose of supporting the 197 | [NX framework](https://github.com/nx-js/framework). 198 | Features should only be added, if they are used by the framework. Otherwise please fork. 199 | 200 | Bug fixes, tests and doc updates are always welcome. 201 | Tests and linter (standardJS) must pass. 202 | 203 | ## Authors 204 | 205 | - [Miklos Bertalan](https://github.com/solkimicreb) 206 | 207 | # License 208 | 209 | MIT 210 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nx-js/compiler-util", 3 | "version": "2.0.0", 4 | "description": "An NX util, responsible for executing code in the context of an object.", 5 | "main": "dist/cjs.es5.js", 6 | "module": "dist/es.es5.js", 7 | "scripts": { 8 | "build": "node ./scripts/build.js", 9 | "test": "node ./scripts/test.js", 10 | "test-builds": "node ./scripts/testBuilds.js", 11 | "lint": "standard src/* test/*" 12 | }, 13 | "author": { 14 | "name": "Miklos Bertalan", 15 | "email": "miklos.bertalan@risingstack.com" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:nx-js/compiler-util.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/nx-js/compiler-util/issues" 23 | }, 24 | "homepage": "https://github.com/nx-js/compiler-util#readme", 25 | "license": "MIT", 26 | "keywords": [ 27 | "nx", 28 | "util", 29 | "compile", 30 | "context", 31 | "eval", 32 | "code", 33 | "expression" 34 | ], 35 | "devDependencies": { 36 | "babel-core": "^6.26.3", 37 | "babel-loader": "7.1.4", 38 | "babel-minify": "^0.4.3", 39 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 40 | "babel-preset-es2016": "^6.24.1", 41 | "babel-preset-es2017": "^6.24.1", 42 | "babel-preset-stage-0": "^6.24.1", 43 | "buble": "^0.19.3", 44 | "chai": "4.1.2", 45 | "coveralls": "^2.13.1", 46 | "del": "^3.0.0", 47 | "karma": "^2.0.2", 48 | "karma-chai": "^0.1.0", 49 | "karma-chrome-launcher": "^2.2.0", 50 | "karma-coverage": "^1.1.1", 51 | "karma-mocha": "^1.3.0", 52 | "karma-mocha-reporter": "^2.2.5", 53 | "karma-rollup-preprocessor": "^5.0.1", 54 | "karma-source-map-support": "^1.2.0", 55 | "mocha": "5.2.0", 56 | "nyc": "^12.0.2", 57 | "pre-push": "0.1.1", 58 | "rollup": "^0.60.1", 59 | "rollup-plugin-alias": "^1.4.0", 60 | "rollup-plugin-auto-external": "^1.2.0", 61 | "rollup-plugin-babel": "^3.0.4", 62 | "rollup-plugin-commonjs": "^9.1.3", 63 | "rollup-plugin-coverage": "^0.1.4", 64 | "rollup-plugin-node-resolve": "^3.3.0", 65 | "standard": "11.0.1" 66 | }, 67 | "engines": { 68 | "node": ">=6.0.0" 69 | }, 70 | "standard": { 71 | "ignore": [ 72 | "test" 73 | ] 74 | }, 75 | "pre-push": [ 76 | "lint", 77 | "test" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const del = require('del') 4 | const babel = require('babel-core') 5 | const buble = require('buble') 6 | const rollup = require('rollup') 7 | const resolvePlugin = require('rollup-plugin-node-resolve') 8 | const babelPlugin = require('rollup-plugin-babel') 9 | const externalsPlugin = require('rollup-plugin-auto-external') 10 | 11 | const bundles = [ 12 | { 13 | input: { 14 | input: path.resolve('src/index.js'), 15 | plugins: [ 16 | resolvePlugin(), 17 | babelPlugin({ exclude: 'node_modules/**' }), 18 | externalsPlugin({ dependencies: true, peerDependecies: true }) 19 | ] 20 | }, 21 | output: { 22 | format: 'es' 23 | } 24 | }, 25 | { 26 | input: { 27 | input: path.resolve('src/index.js'), 28 | plugins: [ 29 | resolvePlugin(), 30 | babelPlugin({ exclude: 'node_modules/**' }), 31 | externalsPlugin({ dependencies: true, peerDependecies: true }) 32 | ] 33 | }, 34 | output: { 35 | format: 'cjs' 36 | } 37 | } 38 | ] 39 | 40 | async function build () { 41 | // Clean up the output directory 42 | await del(path.resolve('dist')) 43 | fs.mkdirSync(path.resolve('dist')) 44 | 45 | // Compile source code into a distributable format with Babel and Rollup 46 | for (const config of bundles) { 47 | const es6Path = path.resolve('dist', `${config.output.format}.es6.js`) 48 | const bundle = await rollup.rollup(config.input) 49 | const { code: es6Code } = await bundle.generate(config.output) 50 | fs.writeFileSync(es6Path, es6Code, 'utf-8') 51 | 52 | const es6MinPath = path.resolve( 53 | 'dist', 54 | `${config.output.format}.es6.min.js` 55 | ) 56 | const { code: es6MinCode } = babel.transform(es6Code, { 57 | presets: ['minify'] 58 | }) 59 | fs.writeFileSync(es6MinPath, es6MinCode, 'utf-8') 60 | 61 | const es5Path = path.resolve('dist', `${config.output.format}.es5.js`) 62 | const { code: es5Code } = buble.transform(es6Code, { 63 | transforms: { 64 | dangerousForOf: true, 65 | modules: false 66 | } 67 | }) 68 | fs.writeFileSync(es5Path, es5Code, 'utf-8') 69 | 70 | const es5MinPath = path.resolve( 71 | 'dist', 72 | `${config.output.format}.es5.min.js` 73 | ) 74 | const { code: es5MinCode } = babel.transform(es5Code, { 75 | presets: ['minify'] 76 | }) 77 | fs.writeFileSync(es5MinPath, es5MinCode, 'utf-8') 78 | } 79 | } 80 | 81 | build() 82 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const resolve = require('rollup-plugin-node-resolve') 3 | const commonjs = require('rollup-plugin-commonjs') 4 | const babel = require('rollup-plugin-babel') 5 | const coverage = require('rollup-plugin-coverage') 6 | const alias = require('rollup-plugin-alias') 7 | const TestServer = require('karma').Server 8 | 9 | const bundleName = process.env.BUNDLE 10 | const bundlePath = bundleName ? `dist/${bundleName}` : 'src/index.js' 11 | 12 | const config = { 13 | frameworks: ['mocha', 'chai', 'source-map-support'], 14 | reporters: ['mocha', 'coverage'], 15 | files: ['tests/**/*.test.js'], 16 | preprocessors: { 17 | 'tests/**/*.test.js': ['rollup'] 18 | }, 19 | rollupPreprocessor: { 20 | plugins: [ 21 | alias({ 22 | '@nx-js/compiler-util': path.resolve(bundlePath) 23 | }), 24 | babel({ 25 | exclude: 'node_modules/**' 26 | }), 27 | resolve(), 28 | commonjs({ 29 | namedExports: { 30 | 'node_modules/chai/index.js': ['expect'] 31 | } 32 | }), 33 | coverage({ 34 | include: ['src/**/*.js'] 35 | }) 36 | ], 37 | output: { 38 | format: 'iife', 39 | name: 'compiler', 40 | sourcemap: 'inline' 41 | } 42 | }, 43 | coverageReporter: { 44 | dir: 'coverage', 45 | reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }] 46 | }, 47 | port: 9876, 48 | colors: true, 49 | autoWatch: false, 50 | concurrency: Infinity, 51 | singleRun: true, 52 | browsers: ['ChromeHeadlessNoSandbox'], 53 | customLaunchers: { 54 | ChromeHeadlessNoSandbox: { 55 | base: 'ChromeHeadless', 56 | flags: ['--no-sandbox'] 57 | } 58 | } 59 | } 60 | 61 | const testServer = new TestServer(config, exitCode => { 62 | console.log(`Karma has exited with ${exitCode}`) 63 | process.exit(exitCode) 64 | }) 65 | testServer.start() 66 | -------------------------------------------------------------------------------- /scripts/testBuilds.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { exec } = require('child_process') 4 | 5 | const distPath = path.resolve('dist') 6 | const files = fs.readdirSync(distPath) 7 | 8 | async function testBuilds () { 9 | for (let file of files) { 10 | const err = await execPromise(`BUNDLE=${file} npm run test`) 11 | if (err) { 12 | console.error('\x1b[31m', `Error in ${file}`, '\x1b[30m') 13 | } else { 14 | console.log(`${file} works as expected`) 15 | } 16 | } 17 | } 18 | 19 | function execPromise (cmd) { 20 | return new Promise(resolve => exec(cmd, resolve)) 21 | } 22 | 23 | testBuilds() 24 | -------------------------------------------------------------------------------- /src/compiler.js: -------------------------------------------------------------------------------- 1 | import { parseExpression, parseCode } from './parser' 2 | 3 | const expressionCache = new Map() 4 | const codeCache = new Map() 5 | 6 | export function compileExpression (src) { 7 | if (typeof src !== 'string') { 8 | throw new TypeError('First argument must be a string.') 9 | } 10 | let expression = expressionCache.get(src) 11 | if (!expression) { 12 | expression = parseExpression(src) 13 | expressionCache.set(src, expression) 14 | } 15 | 16 | if (typeof expression === 'function') { 17 | return expression 18 | } 19 | 20 | return function evaluateExpression (context, tempVars) { 21 | let value = expression.exec(context, tempVars) 22 | for (let filter of expression.filters) { 23 | const args = filter.argExpressions.map(evaluateArgExpression, context) 24 | value = filter.effect(value, ...args) 25 | } 26 | return value 27 | } 28 | } 29 | 30 | export function compileCode (src) { 31 | if (typeof src !== 'string') { 32 | throw new TypeError('First argument must be a string.') 33 | } 34 | let code = codeCache.get(src) 35 | if (!code) { 36 | code = parseCode(src) 37 | codeCache.set(src, code) 38 | } 39 | 40 | if (typeof code === 'function') { 41 | return code 42 | } 43 | 44 | const context = {} 45 | return function evaluateCode (state, tempVars) { 46 | let i = 0 47 | function next () { 48 | Object.assign(context, tempVars) 49 | if (i < code.limiters.length) { 50 | const limiter = code.limiters[i++] 51 | const args = limiter.argExpressions.map(evaluateArgExpression, state) 52 | limiter.effect(next, context, ...args) 53 | } else { 54 | code.exec(state, tempVars) 55 | } 56 | } 57 | next() 58 | } 59 | } 60 | 61 | function evaluateArgExpression (argExpression) { 62 | return argExpression(this) 63 | } 64 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | const hasHandler = { has } 2 | const allHandlers = { has, get } 3 | const globals = new Set() 4 | let temp 5 | 6 | let globalObj 7 | if (typeof window !== 'undefined') globalObj = window // eslint-disable-line 8 | else if (typeof global !== 'undefined') globalObj = global // eslint-disable-line 9 | else if (typeof self !== 'undefined') globalObj = self // eslint-disable-line 10 | globalObj.$nxCompileToSandbox = toSandbox 11 | globalObj.$nxClearSandbox = clearSandbox 12 | 13 | export function expose (...globalNames) { 14 | for (let globalName of globalNames) { 15 | globals.add(globalName) 16 | } 17 | } 18 | 19 | export function hide (...globalNames) { 20 | for (let globalName of globalNames) { 21 | globals.delete(globalName) 22 | } 23 | } 24 | 25 | export function hideAll () { 26 | globals.clear() 27 | } 28 | 29 | function has (target, key) { 30 | return globals.has(key) ? (key in target) : true 31 | } 32 | 33 | function get (target, key) { 34 | return key in temp ? temp[key] : target[key] 35 | } 36 | 37 | function toSandbox (obj, tempVars) { 38 | if (tempVars) { 39 | temp = tempVars 40 | return new Proxy(obj, allHandlers) 41 | } 42 | return new Proxy(obj, hasHandler) 43 | } 44 | 45 | function clearSandbox () { 46 | temp = undefined 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './compiler' 2 | export * from './rawCompiler' 3 | export * from './context' 4 | export * from './modifiers' 5 | -------------------------------------------------------------------------------- /src/modifiers.js: -------------------------------------------------------------------------------- 1 | export const filters = new Map() 2 | export const limiters = new Map() 3 | 4 | export function filter (name, handler) { 5 | if (typeof name !== 'string') { 6 | throw new TypeError('First argument must be a string.') 7 | } 8 | if (typeof handler !== 'function') { 9 | throw new TypeError('Second argument must be a function.') 10 | } 11 | if (filters.has(name)) { 12 | throw new Error(`A filter named ${name} is already registered.`) 13 | } 14 | filters.set(name, handler) 15 | return this 16 | } 17 | 18 | export function limiter (name, handler) { 19 | if (typeof name !== 'string') { 20 | throw new TypeError('First argument must be a string.') 21 | } 22 | if (typeof handler !== 'function') { 23 | throw new TypeError('Second argument must be a function.') 24 | } 25 | if (limiters.has(name)) { 26 | throw new Error(`A limiter named ${name} is already registered.`) 27 | } 28 | limiters.set(name, handler) 29 | return this 30 | } 31 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | import { limiters, filters } from './modifiers' 2 | import { compileRawExpression, compileRawCode } from './rawCompiler' 3 | 4 | const filterRegex = /(?:[^\|]|\|\|)+/g 5 | const limiterRegex = /(?:[^&]|&&)+/g 6 | const argsRegex = /\S+/g 7 | 8 | export function parseExpression (src) { 9 | const tokens = src.match(filterRegex) 10 | if (tokens.length === 1) { 11 | return compileRawExpression(tokens[0]) 12 | } 13 | 14 | const expression = { 15 | exec: compileRawExpression(tokens[0]), 16 | filters: [] 17 | } 18 | for (let i = 1; i < tokens.length; i++) { 19 | let filterTokens = tokens[i].match(argsRegex) 20 | const filterName = filterTokens.shift() 21 | const effect = filters.get(filterName) 22 | if (!effect) { 23 | throw new Error(`There is no filter named: ${filterName}.`) 24 | } 25 | expression.filters.push({effect, argExpressions: filterTokens.map(compileRawExpression)}) 26 | } 27 | return expression 28 | } 29 | 30 | export function parseCode (src) { 31 | const tokens = src.match(limiterRegex) 32 | if (tokens.length === 1) { 33 | return compileRawCode(tokens[0]) 34 | } 35 | 36 | const code = { 37 | exec: compileRawCode(tokens[0]), 38 | limiters: [] 39 | } 40 | for (let i = 1; i < tokens.length; i++) { 41 | const limiterTokens = tokens[i].match(argsRegex) 42 | const limiterName = limiterTokens.shift() 43 | const effect = limiters.get(limiterName) 44 | if (!effect) { 45 | throw new Error(`There is no limiter named: ${limiterName}.`) 46 | } 47 | code.limiters.push({effect, argExpressions: limiterTokens.map(compileRawExpression)}) 48 | } 49 | return code 50 | } 51 | -------------------------------------------------------------------------------- /src/rawCompiler.js: -------------------------------------------------------------------------------- 1 | export function compileRawExpression (src) { 2 | return new Function('context', 'tempVars', // eslint-disable-line 3 | `const sandbox = $nxCompileToSandbox(context, tempVars) 4 | try { with (sandbox) { return ${src} } } catch (err) { 5 | if (!(err instanceof TypeError)) throw err 6 | } 7 | $nxClearSandbox()`) 8 | } 9 | 10 | export function compileRawCode (src) { 11 | return new Function('context', 'tempVars', // eslint-disable-line 12 | `const sandbox = $nxCompileToSandbox(context, tempVars) 13 | with (sandbox) { ${src} } 14 | $nxClearSandbox()`) 15 | } 16 | -------------------------------------------------------------------------------- /tests/a_modifiers.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as compiler from '@nx-js/compiler-util' 3 | 4 | describe('modifiers', () => { 5 | describe('filter', () => { 6 | it('should throw a TypeError on invalid arguments', () => { 7 | expect(() => compiler.filter(12, () => {})).to.throw(TypeError) 8 | expect(() => compiler.filter('name')).to.throw(TypeError) 9 | expect(() => compiler.filter('name', () => {})).to.not.throw 10 | }) 11 | 12 | it('should throw an Error when a filter is already registered by the name', () => { 13 | compiler.filter('filter', () => {}) 14 | expect(() => compiler.filter('filter', () => {})).to.throw(Error) 15 | }) 16 | 17 | it('should register a filter for expressions', () => { 18 | compiler.filter('capitalize', txt => txt.toUpperCase()) 19 | const expression = compiler.compileExpression('message | capitalize') 20 | expect(expression({message: 'hello'})).to.equal('HELLO') 21 | }) 22 | 23 | it('should accept extra arguments', () => { 24 | compiler.filter('slice', (txt, start, end) => txt.slice(start, end)) 25 | const expression = compiler.compileExpression('message | slice start end') 26 | expect(expression({message: 'hello', start: 1, end: 4})).to.equal('ell') 27 | }) 28 | 29 | it('should pipe with other filters', () => { 30 | const expression = compiler.compileExpression('message | slice 1 4 | capitalize') 31 | expect(expression({message: 'hello'})).to.equal('ELL') 32 | }) 33 | 34 | it('should throw an error on using unregistered filters', () => { 35 | expect(() => compiler.compileExpression('message | unregisteredFilter')).to.throw(Error) 36 | }) 37 | }) 38 | 39 | describe('limiter', () => { 40 | it('should throw a TypeError on invalid arguments', () => { 41 | expect(() => compiler.limiter(12, () => {})).to.throw(TypeError) 42 | expect(() => compiler.limiter('name')).to.throw(TypeError) 43 | expect(() => compiler.limiter('name', () => {})).to.not.throw 44 | }) 45 | 46 | it('should throw an Error when a limiter is already registered by the name', () => { 47 | compiler.limiter('limiter', () => {}) 48 | expect(() => compiler.limiter('limiter', () => {})).to.throw(Error) 49 | }) 50 | 51 | it('should register a limiter for code', () => { 52 | compiler.limiter('block', () => {}) 53 | const increment = compiler.compileCode('counter++') 54 | const blocked = compiler.compileCode('counter++ & block') 55 | const context = {counter: 0} 56 | 57 | increment(context) 58 | expect(context.counter).to.equal(1) 59 | blocked(context) 60 | expect(context.counter).to.equal(1) 61 | }) 62 | 63 | it('should accept extra arguments', () => { 64 | compiler.limiter('if', (next, context, condition) => { 65 | if (condition) next() 66 | }) 67 | 68 | const increment = compiler.compileCode('counter++ & if condition') 69 | const context = {counter: 0, condition: true} 70 | 71 | increment(context) 72 | expect(context.counter).to.equal(1) 73 | 74 | context.condition = false 75 | increment(context) 76 | expect(context.counter).to.equal(1) 77 | }) 78 | 79 | it('should allow deferred execution', () => { 80 | compiler.limiter('nextTick', next => Promise.resolve().then(next)) 81 | 82 | const increment = compiler.compileCode('counter++ & nextTick') 83 | const context = {counter: 0, condition: true} 84 | 85 | increment(context) 86 | expect(context.counter).to.equal(0) 87 | 88 | return Promise.resolve() 89 | .then(() => expect(context.counter).to.equal(1)) 90 | }) 91 | 92 | it('should keep a limiter context', () => { 93 | compiler.limiter('blockOdd', (next, context) => { 94 | context.shouldBlock = !context.shouldBlock 95 | if (!context.shouldBlock) next() 96 | }) 97 | 98 | const increment = compiler.compileCode('counter++ & blockOdd') 99 | const context = {counter: 0} 100 | 101 | increment(context) 102 | expect(context.counter).to.equal(0) 103 | 104 | increment(context) 105 | expect(context.counter).to.equal(1) 106 | 107 | increment(context) 108 | expect(context.counter).to.equal(1) 109 | 110 | increment(context) 111 | expect(context.counter).to.equal(2) 112 | 113 | increment(context) 114 | expect(context.counter).to.equal(2) 115 | }) 116 | 117 | it('shoulds should not share limiter context between codes', () => { 118 | const increment1 = compiler.compileCode('counter++ & blockOdd') 119 | const increment2 = compiler.compileCode('counter++ & blockOdd') 120 | const context = {counter: 0} 121 | 122 | increment1(context) 123 | expect(context.counter).to.equal(0) 124 | 125 | increment2(context) 126 | expect(context.counter).to.equal(0) 127 | 128 | increment1(context) 129 | expect(context.counter).to.equal(1) 130 | 131 | increment2(context) 132 | expect(context.counter).to.equal(2) 133 | }) 134 | }) 135 | 136 | it('should throw an error on using unregistered limiters', () => { 137 | expect(() => compiler.compileCode('return message & unregisteredLimiter')).to.throw(Error) 138 | }) 139 | }) 140 | -------------------------------------------------------------------------------- /tests/compiler.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as compiler from '@nx-js/compiler-util' 3 | 4 | const localProp = 'localProp' 5 | window.globalProp = 'globalProp' 6 | 7 | describe('compiler', () => { 8 | describe('compileCode()', () => { 9 | it('should throw a TypeError on non string source argument', () => { 10 | expect(() => compiler.compileCode({})).to.throw(TypeError) 11 | expect(() => compiler.compileCode(undefined)).to.throw(TypeError) 12 | expect(() => compiler.compileCode(12)).to.throw(TypeError) 13 | }) 14 | }) 15 | 16 | describe('compileExpression()', () => { 17 | it('should throw a TypeError on non string source argument', () => { 18 | expect(() => compiler.compileExpression({})).to.throw(TypeError) 19 | expect(() => compiler.compileExpression(undefined)).to.throw(TypeError) 20 | expect(() => compiler.compileExpression(12)).to.throw(TypeError) 21 | }) 22 | }) 23 | 24 | describe('returned function (compiled code or expression)', () => { 25 | it('should throw a TypeError on non object sandbox argument', () => { 26 | const expression = compiler.compileExpression('prop1 + prop2') 27 | expect(() => expression()).to.throw(TypeError) 28 | expect(() => expression('string')).to.throw(TypeError) 29 | expect(() => expression(12)).to.throw(TypeError) 30 | }) 31 | 32 | it('should execute in the context of the sandbox', () => { 33 | const expression = compiler.compileExpression('prop1 + prop2') 34 | expect(expression({prop1: 1, prop2: 2})).to.equal(3) 35 | }) 36 | 37 | it('should not expose local variables', () => { 38 | const expression = compiler.compileExpression('localProp') 39 | expect(expression({})).to.equal(undefined) 40 | }) 41 | 42 | it('should not expose global variables', () => { 43 | const expression = compiler.compileExpression('globalProp') 44 | expect(expression({})).to.equal(undefined) 45 | }) 46 | }) 47 | 48 | describe('returned function expression', () => { 49 | it('should return undefined instead of throwing on invalid property access', () => { 50 | const expression = compiler.compileExpression('inner.prop1') 51 | expect(() => expression({})).to.not.throw(TypeError) 52 | expect(expression({})).to.equal(undefined) 53 | }) 54 | }) 55 | 56 | describe('returned code expression', () => { 57 | it('should throw on invalid property access', () => { 58 | const code = compiler.compileCode('inner.prop1') 59 | expect(() => code({})).to.throw(TypeError) 60 | }) 61 | 62 | it('should accept temporary variables', () => { 63 | const code = compiler.compileCode('return tempVar') 64 | expect(code({}, {tempVar: 'temp'})).to.equal('temp') 65 | }) 66 | 67 | it('should favour temporary variables over persistent ones', () => { 68 | const code = compiler.compileCode('return tempVar') 69 | expect(code({tempVar: 'persistent'}, {tempVar: 'temp'})).to.equal('temp') 70 | }) 71 | 72 | it('should not modify the passed context persistently', () => { 73 | const code = compiler.compileCode('return tempVar') 74 | const context = {} 75 | const result = code(context, {tempVar: 'temp'}) 76 | expect(result).to.equal('temp') 77 | expect(context.tempVar).to.be.undefined 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /tests/context.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import * as compiler from '@nx-js/compiler-util' 3 | 4 | // this file fails if any other test case come after this! 5 | // TODO: figure out why 6 | describe('context', () => { 7 | afterEach(() => compiler.hideAll()) 8 | 9 | describe('expose', () => { 10 | it('should expose the passed global', () => { 11 | const expression = compiler.compileExpression('Math') 12 | expect(expression({})).to.be.undefined 13 | compiler.expose('Math', 'console') 14 | expect(expression({})).to.equal(Math) 15 | }) 16 | 17 | it('should favour sandbox props over exposed globals', () => { 18 | compiler.expose('console') 19 | const expression = compiler.compileExpression('console') 20 | expect(expression({ console: 'prop' })).to.equal('prop') 21 | }) 22 | }) 23 | 24 | describe('hide', () => { 25 | it('should hide exposed globals', () => { 26 | compiler.expose('Math', 'console') 27 | const expression = compiler.compileExpression('console') 28 | expect(expression({})).to.equal(console) 29 | compiler.hide('Math', 'console') 30 | expect(expression({})).to.be.undefined 31 | }) 32 | }) 33 | 34 | describe('hideAll', () => { 35 | it('should hide all globals', () => { 36 | compiler.expose('Math', 'console') 37 | const expression = compiler.compileExpression('console') 38 | expect(expression({})).to.equal(console) 39 | compiler.hideAll() 40 | expect(expression({})).to.be.undefined 41 | }) 42 | }) 43 | }) 44 | --------------------------------------------------------------------------------