diff --git a/.pnp.cjs b/.pnp.cjs index 7ed20f48c..a5623713a 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4988,7 +4988,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["jest", "npm:26.6.3"], ["js-yaml", "npm:4.1.0"], ["mingo", "npm:4.1.2"], - ["quickjs-emscripten", "npm:0.11.0"], ["uuid", "npm:8.3.2"] ], "linkType": "SOFT", @@ -22638,15 +22637,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD", }] ]], - ["quickjs-emscripten", [ - ["npm:0.11.0", { - "packageLocation": "./.yarn/cache/quickjs-emscripten-npm-0.11.0-4f09eb00c3-a5cf45a1fd.zip/node_modules/quickjs-emscripten/", - "packageDependencies": [ - ["quickjs-emscripten", "npm:0.11.0"] - ], - "linkType": "HARD", - }] - ]], ["raf", [ ["npm:3.4.1", { "packageLocation": "./.yarn/cache/raf-npm-3.4.1-c25d48d76e-567b0160be.zip/node_modules/raf/", diff --git a/.yarn/cache/quickjs-emscripten-npm-0.11.0-4f09eb00c3-a5cf45a1fd.zip b/.yarn/cache/quickjs-emscripten-npm-0.11.0-4f09eb00c3-a5cf45a1fd.zip deleted file mode 100644 index 943533f76..000000000 Binary files a/.yarn/cache/quickjs-emscripten-npm-0.11.0-4f09eb00c3-a5cf45a1fd.zip and /dev/null differ diff --git a/packages/docs/menus.yaml b/packages/docs/menus.yaml index 3aa242ab6..2bf18e62b 100644 --- a/packages/docs/menus.yaml +++ b/packages/docs/menus.yaml @@ -609,6 +609,9 @@ - id: _list_contexts type: MenuLink pageId: _list_contexts + - id: _location + type: MenuLink + pageId: _location - id: _log type: MenuLink pageId: _log diff --git a/packages/docs/operators/_js.yaml b/packages/docs/operators/_js.yaml index ec8c337cd..cb5b700d7 100644 --- a/packages/docs/operators/_js.yaml +++ b/packages/docs/operators/_js.yaml @@ -36,20 +36,6 @@ _ref: - A list of arguments can be passed to the JavaScript function which will be spread as function parameters. - The returned value of the custom JavaScript function will be the operator result. - ----------- - - > DEPRECATION WARNING: The QuickJS JavaScript operators are depreciated and will be removed in the next version. Instead native browser functions can now be loaded at build time by registering the operator function with `window.lowdefy.registerJsOperator(name: string, operatorFunction: function)`. See the [Custom Code](/custom-code) section for details. - - For depreciated `evaluate`, and `function`, the following applies to the JavaScript function definitions: - - The `code` operator argument requires a function definition. - - Function arguments can be used inside the function, and are passed via the `args` operator argument as a array. - - A primitive or JSON result will be returned, so the function result must be JSON serializable. - - Only a limited subset of the browser JavaScript APIs are available, in particular the JavaScript inside the VM will not have network access. - - Dependencies and file imports are not supported. - - The function should be a pure function with no side effects. The function should be stateless, and should always return the same result if given the same input. - - > The JavaScript function can be passed to the `code` argument during build using the `_ref.eval` method. See the examples below and the [`_ref`](/_ref) operator for more details. - methods: - name: '{{ method_name }}' types: | @@ -201,232 +187,3 @@ _ref: label: rotate: 0 ```` - - - name: evaluate [DEPRECATED] - types: | - ``` - ({ - code: string, - args?: any[] - }): any - ``` - description: | - The `_js.evaluate` method evaluates JavaScript, takes an array of `args` as input for parameters, and returns the evaluated result. - A JavaScript function string must be provided to the `code` argument. - - arguments: | - ###### code - The JavaScript function as a string to evaluate. - - ###### args - An array of input arguments to pass to the JavaScript function in the order in which the function arguments are defined. - - examples: | - ###### JavaScript evaluate a inline function: - ```yaml - _js.evaluate: - code: | - function makePrimes(to) { - return [...Array(to-1).keys()].map(i=>i+2).filter(n => - [...Array(n-2).keys()].map(i=>i+2).reduce((acc,x)=> acc && n % x !== 0, true) - } - args: - - 50 - ``` - Returns: - ``` - [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] - ``` - - ###### JavaScript evaluate loaded from a JavaScript file: - Operator definition: - ```yaml - _js.evaluate: - code: - _ref: - eval: myMath/makePrimes.js - args: - - 50 - ``` - JavaScript loaded from a file using the `_ref.eval` operator method: - ```js - // myMath/makePrimes.js - function makePrimes(to) { - return [...Array(to-1).keys()].map(i=>i+2).filter(n => - [...Array(n-2).keys()].map(i=>i+2).reduce((acc,x)=> acc && n % x !== 0, true) - } - module.exports = makePrimes; - ``` - Returns: - ``` - [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47] - ``` - - name: function [DEPRECATED] - types: | - ``` - ({ - code: string, - }): any - ``` - description: | - The `_js.function` method returns a JavaScript function. The JavaScript function definition is be provided as a string to the `code` operator argument. - - arguments: | - ###### code - The JavaScript function as a string to evaluate. - - examples: | - ###### _js.function to create a label.formatter function for an EChart block: - ```yaml - id: chart - type: EChart - properties: - height: 600 - option: - series: - - radius: - - '15%' - - '80%' - type: 'sunburst' - sort: null - emphasis: - focus: 'ancestor' - data: - - value: 8, - children: - - value: 4, - children: - - value: 2 - - value: 1 - - value: 1 - - value: 0.5 - - value: 2 - - value: 4 - children: - - children: - - value: 2 - - value: 4 - children: - - children: - - value: 2 - - value: 3 - children: - - children: - - value: 1 - label: - color: '#000' - textBorderColor: '#fff' - textBorderWidth: 2 - formatter: - _js.function: - code: | - function (param) { - var depth = param.treePathInfo.length; - if (depth === 2) { - return 'radial'; - } - else if (depth === 3) { - return 'tangential'; - } - else if (depth === 4) { - return '0'; - } - } - levels: - - {} - - itemStyle: - color: '#CD4949' - label: - rotate: 'radial' - - itemStyle: - color: '#F47251' - label: - rotate: 'tangential' - - itemStyle: - color: '#FFC75F' - label: - rotate: 0 - ``` - - ###### _js.function to create a label.formatter function for an EChart block loaded using the `_ref.eval` operator: - - The chart config: - ```yaml - id: chart - type: EChart - properties: - height: 600 - option: - series: - - radius: - - '15%' - - '80%' - type: 'sunburst' - sort: null - emphasis: - focus: 'ancestor' - data: - - value: 8, - children: - - value: 4, - children: - - value: 2 - - value: 1 - - value: 1 - - value: 0.5 - - value: 2 - - value: 4 - children: - - children: - - value: 2 - - value: 4 - children: - - children: - - value: 2 - - value: 3 - children: - - children: - - value: 1 - label: - color: '#000' - textBorderColor: '#fff' - textBorderWidth: 2 - formatter: - _js.function: - code: - _ref: - eval: 'foo/fooFormatter.js' - levels: - - {} - - itemStyle: - color: '#CD4949' - label: - rotate: 'radial' - - itemStyle: - color: '#F47251' - label: - rotate: 'tangential' - - itemStyle: - color: '#FFC75F' - label: - rotate: 0 - ``` - - JavaScript loaded from a file using the `_ref.eval` operator method: - ```js - // foo/fooFormatter.js - function fooFormatter(param) { - var depth = param.treePathInfo.length; - if (depth === 2) { - return 'radial'; - } - else if (depth === 3) { - return 'tangential'; - } - else if (depth === 4) { - return '0'; - } - } - - module.exports = fooFormatter; - ``` diff --git a/packages/docs/operators/_location.yaml b/packages/docs/operators/_location.yaml new file mode 100644 index 000000000..b4e255bb8 --- /dev/null +++ b/packages/docs/operators/_location.yaml @@ -0,0 +1,56 @@ +# Copyright 2020-2021 Lowdefy, Inc + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_ref: + path: templates/operators.yaml.njk + transformer: templates/operatorsMethodTransformer.js + vars: + pageId: _location + pageTitle: _location + filePath: operators/_location.yaml + env: Client Only + types: | + ``` + (key: string): any + ``` + description: | + The `_location` operator gets a value from the browser [Location](https://developer.mozilla.org/en-US/docs/Web/API/Location) interface. The Location interface represents the location (URL) of the window object it is linked to, thus can return the URL elements of the current window. It can only be used on the web-client (Not in `requests` or `connections`). + + The following location properties are available. + - `hash: string`: A string containing a '#' followed by the fragment identifier of the URL. + - `host: string`: A string containing the host, that is the hostname, a `:`, and the port of the URL. + - `hostname: string`: The domain of the URL. + - `href: string`: The entire URL string. + - `origin: string`: The canonical form of the origin of the specific location.. + - `pathname: string`: A string containing an initial `/`` followed by the path of the URL, not including the query string or fragment. + - `port: string`: The port number of the URL + - `protocol: string`: The protocol scheme of the URL, mostly `http:` or `https:`. + - `search: string`: A string containing a '?' followed by the parameters or "querystring" of the URL. + + arguments: | + ###### string + If called with a string argument, the value of the key in the `location` object is returned. If the value is defined, `null` is returned. + + examples: | + ###### Get the `origin` from `location`: + ```yaml + _location: origin + ``` + Returns: The current `window.location.origin`, in this case 'https://docs.lowdefy.com'. + + ###### Get the `pathname` from `location`: + ```yaml + _location: pathname + ``` + Returns: The current `window.location.pathname`, in this case '/_location'. diff --git a/packages/docs/pages.yaml b/packages/docs/pages.yaml index 7f74bc2f2..c39ecf0e3 100644 --- a/packages/docs/pages.yaml +++ b/packages/docs/pages.yaml @@ -167,6 +167,7 @@ - _ref: operators/_js.yaml - _ref: operators/_json.yaml - _ref: operators/_list_contexts.yaml +- _ref: operators/_location.yaml - _ref: operators/_log.yaml - _ref: operators/_lt.yaml - _ref: operators/_lte.yaml diff --git a/packages/operators/package.json b/packages/operators/package.json index c86a434dd..159e19c84 100644 --- a/packages/operators/package.json +++ b/packages/operators/package.json @@ -42,7 +42,6 @@ "deep-diff": "1.0.2", "js-yaml": "4.1.0", "mingo": "4.1.2", - "quickjs-emscripten": "0.11.0", "uuid": "8.3.2" }, "devDependencies": { diff --git a/packages/operators/src/web/index.js b/packages/operators/src/web/index.js index bdce6caec..e9abae650 100644 --- a/packages/operators/src/web/index.js +++ b/packages/operators/src/web/index.js @@ -20,6 +20,7 @@ export default { _base64: 'web/base64', _format: 'web/format', _list_contexts: 'web/list_contexts', + _location: 'web/location', _js: 'web/js', _media: 'web/media', _menu: 'web/menu', diff --git a/packages/operators/src/web/js.js b/packages/operators/src/web/js.js index 1fa6fdaba..508a2286e 100644 --- a/packages/operators/src/web/js.js +++ b/packages/operators/src/web/js.js @@ -16,126 +16,14 @@ import { type } from '@lowdefy/helpers'; -// ! --------------- -// ! DEPRECATED -// ! --------------- -import { serializer } from '@lowdefy/helpers'; -import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten'; -let QuickJsVm; - -function createFunction({ params, location, methodName }) { - if (!params.code || !type.isString(params.code)) { - throw new Error( - `Operator Error: _js.${methodName} "code" argument should be a string at ${location}.` - ); - } - const fn = (...args) => { - const jsFnString = ` - var logs = []; - function log(...item) { - logs.push(item); - } - var console = { - log, - }; - function dateReviver(key, value) { - if (typeof value === 'object' && value !== null && value.hasOwnProperty('_date')) { - var date = new Date(value._date); - if (date instanceof Date && !Number.isNaN(date.getTime())) { - return date; - } - } - return value; - } - function dateSerialize(key, value) { - if (typeof value === 'object' && value !== null) { - Object.keys(value).forEach((k) => { - if (value[k] instanceof Date && !Number.isNaN(value[k].getTime())) { - value[k] = { "_date": value[k].valueOf() }; - } - }) - } - return value; - } - var args = JSON.parse(decodeURIComponent("${encodeURIComponent( - serializer.serializeToString(args) - )}"), dateReviver); - var fnResult = (${params.code})(...(args || [])); - var result = encodeURIComponent(JSON.stringify([fnResult, logs], dateSerialize)); - `; - const codeHandle = QuickJsVm.unwrapResult( - QuickJsVm.evalCode(jsFnString, { - shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 1000), - memoryLimitBytes: 1024 * 1024, - }) - ); - const resultHandle = QuickJsVm.getProp(QuickJsVm.global, 'result'); - codeHandle.dispose(); - const result = serializer.deserializeFromString( - decodeURIComponent(QuickJsVm.getString(resultHandle)) - ); - result[1].forEach((item) => { - console.log(...item); - }); - return result[0]; - }; - return fn; -} - -function evaluate({ params, location, methodName }) { - if (!type.isArray(params.args) && !type.isNone(params.args)) { - throw new Error( - `Operator Error: _js.evaluate "args" argument should be an array, null or undefined at ${location}.` - ); - } - const fn = createFunction({ params, location, methodName }); - return fn(...(params.args || [])); -} - -const methods = { evaluate, function: createFunction }; - -function _DEPRECATED_js({ params, location, methodName }) { - if (!QuickJsVm) { - throw new Error( - `Operator Error: _js is not initialized. Received: ${JSON.stringify(params)} at ${location}.` - ); - } - - return methods[methodName]({ params, location, methodName }); -} - -async function init() { - const QuickJs = await getQuickJS(); - QuickJsVm = QuickJs.createVm(); -} -async function clear() { - QuickJsVm = null; -} - -_js.init = init; -_js.clear = clear; -// ! --------------- - function _js({ context, params, location, methodName }) { - // ! DEPRECATED methods - if (!type.isFunction(context.lowdefy.imports.jsOperators[methodName]) && !methods[methodName]) { + if (!type.isFunction(context.lowdefy.imports.jsOperators[methodName])) { throw new Error(`Operator Error: _js.${methodName} is not a function.`); } - if (context.lowdefy.imports.jsOperators[methodName]) { - if (!type.isNone(params) && !type.isArray(params)) { - throw new Error(`Operator Error: _js.${methodName} takes an array as input at ${location}.`); - } - return context.lowdefy.imports.jsOperators[methodName](...(params || [])); + if (!type.isNone(params) && !type.isArray(params)) { + throw new Error(`Operator Error: _js.${methodName} takes an array as input at ${location}.`); } - // ! DEPRECATED --------------- - console.warn( - 'WARNING: _js.evaluate and _js.function will has been deprecated and will be removed in the next version. Please see: https://docs.lowdefy.com/_js for more details.' - ); - if (!type.isObject(params)) { - throw new Error(`Operator Error: _js.${methodName} takes an object as input at ${location}.`); - } - return _DEPRECATED_js({ params, location, methodName }); - // ! --------------- + return context.lowdefy.imports.jsOperators[methodName](...(params || [])); } export default _js; diff --git a/packages/operators/src/web/location.js b/packages/operators/src/web/location.js new file mode 100644 index 000000000..ebe2e8ef1 --- /dev/null +++ b/packages/operators/src/web/location.js @@ -0,0 +1,57 @@ +/* + Copyright 2020-2021 Lowdefy, Inc + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import getFromObject from '../getFromObject'; + +const validProperties = [ + 'href', + 'origin', + 'protocol', + 'host', + 'hostname', + 'port', + 'pathname', + 'search', + 'hash', +]; +function _location({ arrayIndices, context, contexts, env, location, params }) { + if (!window || !window.location) { + throw new Error( + `Operator Error: Browser window.location not available for _location. Received: ${JSON.stringify( + params + )} at ${location}.` + ); + } + if (!validProperties.includes(params)) { + throw new Error( + `Operator Error: _location only returns values for ${validProperties.join( + ', ' + )}. Received: ${JSON.stringify(params)} at ${location}.` + ); + } + return getFromObject({ + arrayIndices, + context, + contexts, + env, + location, + object: window.location, + operator: '_location', + params, + }); +} + +export default _location; diff --git a/packages/operators/test/WebParser.test.js b/packages/operators/test/WebParser.test.js index 44d203ea7..037c8436a 100644 --- a/packages/operators/test/WebParser.test.js +++ b/packages/operators/test/WebParser.test.js @@ -275,6 +275,22 @@ describe('parse operators', () => { expect(res.errors).toMatchInlineSnapshot(`Array []`); }); + test('parse _location operator', async () => { + Object.defineProperty(window, 'location', { + writable: true, + configurable: true, + value: { origin: 'http://test.com' }, + }); + const input = { a: { _location: 'origin' } }; + const parser = new WebParser({ context, contexts }); + await parser.init(); + const res = parser.parse({ input, location: 'locationId' }); + expect(res.output).toEqual({ + a: 'http://test.com', + }); + expect(res.errors).toMatchInlineSnapshot(`Array []`); + }); + test('parse _random operator', async () => { const mathRandomFn = Math.random; Math.random = () => 0.5678; @@ -499,19 +515,18 @@ describe('parse operators', () => { expect(errors).toEqual([]); }); - test('parse _js operator', async () => { + test.only('parse _js operator retuning a function', async () => { + const test_fn = () => (a, b) => a + b; + const mockFn = jest.fn().mockImplementation(test_fn); + context.lowdefy.imports.jsOperators.test_fn = mockFn; const input = { - '_js.function': { - code: `function (a, b){ - return a + b; - }`, - }, + '_js.test_fn': [], }; const parser = new WebParser({ context, contexts }); await parser.init(); const { output, errors } = parser.parse({ input, location: 'locationId' }); expect(output).toBeInstanceOf(Function); - expect(output(1, 2)).toEqual(3); + expect(output(4, 2)).toEqual(6); expect(errors).toEqual([]); }); diff --git a/packages/operators/test/web/js.test.js b/packages/operators/test/web/js.test.js index 45ad58a7e..e712cfe84 100644 --- a/packages/operators/test/web/js.test.js +++ b/packages/operators/test/web/js.test.js @@ -18,9 +18,6 @@ import _js from '../../src/web/js'; import { context } from '../testContext'; const location = 'location'; -beforeAll(async () => { - await _js.init(); -}); test('_js.test_fn and params to return a value', () => { const params = [12, 14]; @@ -59,302 +56,12 @@ test('_js.test_fn params not an array', () => { ); }); -// ! -------------- -// ! DEPRECATED -// ! -------------- -test('_js with code and args specified', () => { - const params = { - code: `function (one, two) { - return one + two; -}`, - args: [12, 14], - }; - const fn = _js({ context, location, params, methodName: 'function' }); - expect(fn).toBeInstanceOf(Function); - expect(fn(1, 2)).toEqual(3); - expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual(26); - expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual( - null +test('_js.not_a_function', () => { + const params = 10; + const test_fn = (a, b) => a + b; + const mockFn = jest.fn().mockImplementation(test_fn); + context.lowdefy.imports.jsOperators.test_fn = mockFn; + expect(() => _js({ context, location, params, methodName: 'not_a_function' })).toThrow( + new Error('Operator Error: _js.not_a_function is not a function.') ); }); - -test('_js with code and args specified to return json object', () => { - const params = { - code: `function (one, two) { - return { a: one, b: two, c: [1,2,3, one, two, "three"]}; -}`, - args: [12, 14], - }; - const fn = _js({ context, location, params, methodName: 'function' }); - expect(fn).toBeInstanceOf(Function); - expect(fn(1, 2)).toEqual({ a: 1, b: 2, c: [1, 2, 3, 1, 2, 'three'] }); - expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual({ - a: 12, - b: 14, - c: [1, 2, 3, 12, 14, 'three'], - }); - expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual( - { - c: [1, 2, 3, null, null, 'three'], - } - ); -}); - -test('_js with code and args specified to return json array', () => { - const params = { - code: `function (one, two) { - return [1,2,3, one, two, "three"]; -}`, - args: [12, 14], - }; - const fn = _js({ context, location, params, methodName: 'function' }); - expect(fn).toBeInstanceOf(Function); - expect(fn(1, 2)).toEqual([1, 2, 3, 1, 2, 'three']); - expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual([ - 1, - 2, - 3, - 12, - 14, - 'three', - ]); - expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual( - [1, 2, 3, null, null, 'three'] - ); -}); - -test('_js with open "\'" in result', () => { - const str = `