├── .npmignore ├── .vscode └── settings.json ├── webpack.config.js ├── .eslintrc.js ├── tasker-imports ├── TJS_UpdateScript.tsk.xml ├── TJS_RunScript.tsk.xml └── TJS_Development_Toggle.tsk.xml ├── package.json ├── LICENSE ├── .gitignore ├── src ├── index.js ├── router.js └── tasker.js ├── test ├── router │ ├── parse-caller-id.spec.js │ └── dispatch.spec.js └── tasker │ └── tasker-utilities │ └── get-locals.spec.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.packageManager": "yarn", 3 | 4 | "editor.tabSize": 2, 5 | "editor.renderWhitespace": "all", 6 | 7 | // Turns auto fix on save on or off. 8 | "eslint.autoFixOnSave": true, 9 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'none', 5 | entry: './src/index.js', 6 | output: { 7 | filename: 'index.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | libraryTarget: 'umd', 10 | library: 'tasker-js-runner', 11 | umdNamedDefine: true, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "parser": "babel-eslint", 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "windows" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ] 29 | } 30 | }; -------------------------------------------------------------------------------- /tasker-imports/TJS_UpdateScript.tsk.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1519533460618 4 | 1519573304814 5 | 62 6 | TJS:UpdateScript 7 | 100 8 | 9 | 129 10 | fetch(global('TJS_DEV_REMOTE')) 11 | .then(res => res.text()) 12 | .then((result) => { 13 | writeFile(global('TJS_LOCAL_PATH'), result); 14 | flash('TJS: Script is updated'); 15 | }) 16 | .catch((err) => { 17 | flash(err.message) 18 | }) 19 | .then(exit) 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tasker-imports/TJS_RunScript.tsk.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1514855272770 4 | 1558890701302 5 | 29 6 | TJS:RunScript 7 | 2 8 | 9 | 347 10 | 11 | 12 | 13 | %local_keys 14 | 15 | 16 | 131 17 | false 18 | %TJS_LOCAL_PATH 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tasker-js-runner", 3 | "version": "1.1.0", 4 | "description": "JavaScript for Tasker", 5 | "author": "Amos Wong ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/amoshydra/tasker-js-runner", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:amoshydra/tasker-js-runner.git" 11 | }, 12 | "main": "dist/index.js", 13 | "module": "src/index.js", 14 | "files": [ 15 | "dist", 16 | "src", 17 | "tasker-imports" 18 | ], 19 | "scripts": { 20 | "build": "cross-env NODE_ENV=production webpack", 21 | "test": "ava" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.4.5", 25 | "@babel/preset-env": "^7.4.5", 26 | "ava": "^1.4.1", 27 | "babel-eslint": "^10.0.1", 28 | "babel-loader": "8.0.6", 29 | "cross-env": "^5.2.0", 30 | "eslint": "^5.16.0", 31 | "esm": "^3.2.25", 32 | "sinon": "^7.3.2", 33 | "webpack": "^4.32.2", 34 | "webpack-cli": "^3.3.2" 35 | }, 36 | "dependencies": {}, 37 | "ava": { 38 | "require": [ 39 | "esm" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Amos Wong Wen Jet 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | 3 | # Created by https://www.gitignore.io/api/node,visualstudiocode 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (http://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | 65 | 66 | ### VisualStudioCode ### 67 | .vscode/* 68 | !.vscode/settings.json 69 | !.vscode/tasks.json 70 | !.vscode/launch.json 71 | !.vscode/extensions.json 72 | .history 73 | 74 | 75 | # End of https://www.gitignore.io/api/node,visualstudiocode -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { initializeTaskerJs } from './tasker'; 2 | import Router from './router'; 3 | 4 | window.tasker = initializeTaskerJs(window); 5 | 6 | const CONFIG = { 7 | Environment: tasker.global('TJS_ENV'), 8 | RemoteUrl: tasker.global('TJS_DEV_REMOTE'), 9 | LocalPath: tasker.global('TJS_LOCAL_PATH'), 10 | }; 11 | const TASK = { 12 | RunScript: 'TJS:RunScript', 13 | }; 14 | 15 | const hotReload = () => { 16 | if (CONFIG.Environment !== 'development') return Promise.resolve(); 17 | 18 | return fetch(CONFIG.RemoteUrl) 19 | .then(res => res.text()) 20 | .then((result) => { 21 | const existingFile = tasker.readFile(CONFIG.LocalPath); 22 | 23 | if (existingFile !== result) { 24 | tasker.writeFile(CONFIG.LocalPath, result); 25 | tasker.flash('script updated'); 26 | tasker.performTask( 27 | /* Task name */ TASK.RunScript, 28 | /* Priority */tasker.local('priority'), 29 | /* par1 */ 'null', 30 | /* par2 */ JSON.stringify(tasker.locals), // Supply par2 to overwrite context 31 | ); 32 | tasker.exit(); 33 | } 34 | 35 | }) 36 | .catch(err => tasker.flash(err.message)); 37 | }; 38 | 39 | export default class TaskerJS { 40 | constructor(routes) { 41 | this.router = new Router(routes, tasker); 42 | 43 | hotReload() 44 | .then(() => 45 | this.router.dispatch(tasker.locals) 46 | .catch(err => tasker.flash(err.message)) 47 | .then(() => tasker.exit()) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | export const ROUTE_TYPE = { 2 | Enter: 'enter', 3 | Exit: 'exit', 4 | }; 5 | 6 | export const parseCallerId = (callerId = '') => { 7 | const [callerSourceId, ...splitedRouteId] = callerId.split('='); 8 | const routeId = splitedRouteId.join('='); 9 | 10 | switch (callerSourceId) { 11 | case 'profile': { 12 | const [callerType, ...splittedCallerRoute] = routeId.split(':'); 13 | return { 14 | type: callerType, 15 | route: splittedCallerRoute.join(':'), 16 | }; 17 | } 18 | case 'task': return { 19 | type: ROUTE_TYPE.Enter, 20 | route: routeId, 21 | }; 22 | default: return { 23 | type: ROUTE_TYPE.Enter, 24 | route: callerId, 25 | }; 26 | } 27 | } 28 | 29 | export default class Router { 30 | constructor(routes, context) { 31 | this.context = context; 32 | this.routes = routes; 33 | if (!this.routes._errorHandler) { 34 | this.routes._errorHandler = { 35 | enter() { 36 | context.console.log('No route matched') 37 | }, 38 | exit() {}, 39 | }; 40 | } 41 | } 42 | 43 | dispatch(locals) { 44 | return Promise.resolve() 45 | .then(() => { 46 | // Make route 47 | const callerId = locals.caller && locals.caller[locals.caller.length - 1]; 48 | const caller = parseCallerId(callerId); 49 | 50 | // Go to route 51 | const route = this.routes[caller.route] || this.routes._errorHandler; 52 | return route[caller.type](locals, this.context); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/router/parse-caller-id.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { 3 | parseCallerId, 4 | ROUTE_TYPE, 5 | } from '../../src/router'; 6 | 7 | test('should parse Profile enter syntax', t => { 8 | const caller = parseCallerId('profile=enter:ProfileName'); 9 | 10 | t.deepEqual(caller, { 11 | type: ROUTE_TYPE.Enter, 12 | route: 'ProfileName' 13 | }); 14 | }); 15 | 16 | test('should parse Profile enter syntax with multiple equal and colon characters', t => { 17 | const caller = parseCallerId('profile=enter:ProfileName=:=Name'); 18 | 19 | t.deepEqual(caller, { 20 | type: ROUTE_TYPE.Enter, 21 | route: 'ProfileName=:=Name' 22 | }); 23 | }); 24 | 25 | test('should parse Profile exit syntax', t => { 26 | const caller = parseCallerId('profile=exit:ProfileName'); 27 | 28 | t.deepEqual(caller, { 29 | type: ROUTE_TYPE.Exit, 30 | route: 'ProfileName' 31 | }); 32 | }); 33 | 34 | test('should parse Task syntax', t => { 35 | const caller = parseCallerId('task=TaskName'); 36 | 37 | t.deepEqual(caller, { 38 | type: ROUTE_TYPE.Enter, 39 | route: 'TaskName' 40 | }); 41 | }); 42 | 43 | test('should parse UI syntax', t => { 44 | const caller = parseCallerId('ui'); 45 | 46 | t.deepEqual(caller, { 47 | type: ROUTE_TYPE.Enter, 48 | route: 'ui' 49 | }); 50 | }); 51 | 52 | test('should parse arbitary caller syntax', t => { 53 | t.is( 54 | parseCallerId('Do something').route, 55 | 'Do something' 56 | ); 57 | 58 | t.is( 59 | parseCallerId('Light:On').route, 60 | 'Light:On' 61 | ); 62 | 63 | t.is( 64 | parseCallerId('NoneProfileOrTask=Name').route, 65 | 'NoneProfileOrTask=Name' 66 | ); 67 | t.is( 68 | parseCallerId('NoneProfileOrTask=Enter:Name').route, 69 | 'NoneProfileOrTask=Enter:Name' 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/tasker.js: -------------------------------------------------------------------------------- 1 | export const taskerUtilities = { 2 | inspect: (target) => { 3 | const cache = []; 4 | return JSON.stringify(target, function(key, value) { 5 | if (typeof value === 'object' && value !== null) { 6 | if (cache.indexOf(value) !== -1) { 7 | // Circular reference found, discard key 8 | return; 9 | } 10 | // Store value in our collection 11 | cache.push(value); 12 | } 13 | return value; 14 | }); 15 | }, 16 | 17 | makeConsole: (context) => ({ 18 | log(...params) { 19 | context.flash( 20 | params 21 | .map(param => (typeof param === 'string') ? param : taskerUtilities.inspect(param)) 22 | .join(' ') 23 | ); 24 | }, 25 | }), 26 | 27 | getParams: (context) => { 28 | return (context.par || []) 29 | .map((rawParam) => { 30 | // Test if param is a json 31 | let parsedParam; 32 | try { 33 | parsedParam = JSON.parse(rawParam); // will fail if param is not a JSON 34 | } catch (err) { 35 | parsedParam = rawParam; 36 | } 37 | return parsedParam === 'undefined' ? undefined : parsedParam; 38 | }); 39 | }, 40 | 41 | getLocals: (context) => { 42 | const taskParameters = taskerUtilities.getParams(context); 43 | 44 | // Handle overriding behaviour 45 | const par2 = taskParameters[1]; 46 | const overridingLocals = ((par2 && (typeof par2 === 'object')) ? taskParameters[1] : null); 47 | if (overridingLocals) { 48 | return { 49 | par: (overridingLocals.par || []), 50 | caller: (overridingLocals.caller || []), 51 | ...overridingLocals, 52 | }; 53 | } 54 | 55 | // Handle merging behaviour 56 | const par1 = taskParameters[0]; 57 | const parentLocals = ((par1 && (typeof par1 === 'object')) ? par1 : {}) || {}; 58 | const locals = (context.local_keys || []) 59 | .reduce((acc, key) => { 60 | const keyName = key.slice(1); 61 | acc[keyName] = context.local(keyName); 62 | return acc; 63 | }, {}); 64 | 65 | 66 | return ({ 67 | ...locals, 68 | ...parentLocals, 69 | par: taskParameters, 70 | caller: [ 71 | ...(context.caller || []), 72 | ...(parentLocals.caller || []), 73 | ], 74 | }) 75 | }, 76 | }; 77 | 78 | export const initializeTaskerJs = (context) => { 79 | // Injecting development functions 80 | context.inspect = taskerUtilities.inspect; 81 | context.console = taskerUtilities.makeConsole(context); 82 | 83 | // Attempt to restore param from upstream 84 | context.locals = taskerUtilities.getLocals(context) 85 | 86 | return context; 87 | } 88 | -------------------------------------------------------------------------------- /test/tasker/tasker-utilities/get-locals.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { taskerUtilities } from '../../../src/tasker'; 3 | 4 | class Context { 5 | constructor(context) { 6 | Object.keys(context).forEach(key => this[key] = context[key]); 7 | } 8 | 9 | local(name) { 10 | return this[name]; 11 | } 12 | } 13 | 14 | test('should output a minimal locals containing caller and par', t => { 15 | const actual = taskerUtilities.getLocals( 16 | new Context({}) 17 | ); 18 | 19 | t.deepEqual(actual, { 20 | caller: [], 21 | par: [], 22 | }) 23 | }); 24 | 25 | test('should merge own locals with parent\'s locals', t => { 26 | const parentLocal = JSON.stringify({ 27 | localA: 'localA', 28 | localB: 'localB', 29 | conflictA: 'parentConflicA', 30 | }); 31 | const actual = taskerUtilities.getLocals( 32 | new Context({ 33 | caller: ['caller3', 'caller2', 'caller1'], 34 | par: [ 35 | { 36 | localA: 'localA', 37 | localB: 'localB', 38 | conflictA: 'parentConflicA', 39 | }, 40 | ], 41 | conflictA: 'selfConflicA', 42 | }) 43 | ); 44 | 45 | t.deepEqual(actual, { 46 | caller: ['caller3', 'caller2', 'caller1'], 47 | par: [ 48 | JSON.parse(parentLocal), 49 | ], 50 | localA: 'localA', 51 | localB: 'localB', 52 | conflictA: 'parentConflicA', 53 | }) 54 | }); 55 | 56 | test('should skip parent\'s locals if it is null', t => { 57 | const parentLocal = null; 58 | const actual = taskerUtilities.getLocals( 59 | new Context({ 60 | caller: ['caller3', 'caller2', 'caller1'], 61 | par: [ 62 | parentLocal, 63 | ], 64 | }) 65 | ); 66 | 67 | t.deepEqual(actual, { 68 | caller: ['caller3', 'caller2', 'caller1'], 69 | par: [ 70 | null, 71 | ], 72 | }) 73 | }); 74 | 75 | test('should skip parent\'s locals if it is not an JSON string', t => { 76 | const parentLocal = "INVALID: PARENT"; 77 | const actual = taskerUtilities.getLocals( 78 | new Context({ 79 | caller: ['caller3', 'caller2', 'caller1'], 80 | par: [ 81 | parentLocal, 82 | ], 83 | }) 84 | ); 85 | 86 | t.deepEqual(actual, { 87 | caller: ['caller3', 'caller2', 'caller1'], 88 | par: [ 89 | parentLocal, 90 | ], 91 | }) 92 | }); 93 | 94 | test('should ignore own locals with par2 is provided', t => { 95 | const overridingLocal = JSON.stringify({ 96 | caller: [ 97 | 'OverridingCaller2', 98 | 'OverridingCaller1' 99 | ], 100 | }); 101 | const actual = taskerUtilities.getLocals( 102 | new Context({ 103 | caller: ['caller3', 'caller2', 'caller1'], 104 | par: [ 105 | 'null', 106 | overridingLocal, 107 | ], 108 | }) 109 | ); 110 | 111 | t.deepEqual(actual, { 112 | caller: [ 113 | 'OverridingCaller2', 114 | 'OverridingCaller1' 115 | ], 116 | par: [], 117 | }) 118 | }); 119 | -------------------------------------------------------------------------------- /test/router/dispatch.spec.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import sinon from 'sinon'; 3 | import Router from '../../src/router'; 4 | 5 | test('should dispatch routes._errorHandler.enter function given no caller', async t => { 6 | const context = global; 7 | const router = new Router({}, context); 8 | 9 | const defaultErrorHandlerEnterSpy = sinon.spy(router.routes._errorHandler, 'enter'); 10 | 11 | const locals = { /* locals without caller */ }; 12 | await router.dispatch(locals); 13 | 14 | t.true(defaultErrorHandlerEnterSpy.calledOnceWithExactly(locals, context)); 15 | }); 16 | 17 | test('should dispatch routes._errorHandler.enter function given invalid caller', async t => { 18 | const context = global; 19 | const router = new Router({}, context); 20 | 21 | const defaultErrorHandlerEnterSpy = sinon.spy(router.routes._errorHandler, 'enter'); 22 | 23 | const locals = { 24 | caller: ['profile=enter:AnInvalidCallerId'] 25 | }; 26 | await router.dispatch(locals); 27 | 28 | t.true(defaultErrorHandlerEnterSpy.calledOnceWithExactly(locals, context)); 29 | }); 30 | 31 | test('should dispatch matching Profile enter caller', async t => { 32 | const context = global; 33 | const router = new Router({ 34 | ValidCallerId: { 35 | enter: sinon.fake(), 36 | } 37 | }, context); 38 | 39 | const locals = { 40 | caller: ['profile=enter:ValidCallerId'] 41 | }; 42 | await router.dispatch(locals); 43 | 44 | t.true(router.routes.ValidCallerId.enter.calledOnceWithExactly(locals, context)); 45 | }); 46 | 47 | test('should dispatch matching Profile exit caller', async t => { 48 | const context = global; 49 | const router = new Router({ 50 | ValidCallerId: { 51 | exit: sinon.fake(), 52 | } 53 | }, context); 54 | 55 | const locals = { 56 | caller: ['profile=exit:ValidCallerId'] 57 | }; 58 | await router.dispatch(locals); 59 | 60 | t.true(router.routes.ValidCallerId.exit.calledOnceWithExactly(locals, context)); 61 | }); 62 | 63 | test('should dispatch matching Task caller', async t => { 64 | const context = global; 65 | const router = new Router({ 66 | ValidCallerId: { 67 | enter: sinon.fake(), 68 | } 69 | }, context); 70 | 71 | const locals = { 72 | caller: ['task=ValidCallerId'] 73 | }; 74 | await router.dispatch(locals); 75 | 76 | t.true(router.routes.ValidCallerId.enter.calledOnceWithExactly(locals, context)); 77 | }); 78 | 79 | test('should dispatch matching UI caller', async t => { 80 | const context = global; 81 | const router = new Router({ 82 | ui: { 83 | enter: sinon.fake(), 84 | } 85 | }, context); 86 | 87 | const locals = { 88 | caller: ['ui'] 89 | }; 90 | await router.dispatch(locals); 91 | 92 | t.true(router.routes.ui.enter.calledOnceWithExactly(locals, context)); 93 | }); 94 | 95 | test('should dispatch matching arbitary caller', async t => { 96 | const context = global; 97 | const router = new Router({ 98 | 'NoneProfileOrTask=Enter:Name': { 99 | enter: sinon.fake(), 100 | } 101 | }, context); 102 | 103 | const locals = { 104 | caller: ['NoneProfileOrTask=Enter:Name'] 105 | }; 106 | await router.dispatch(locals); 107 | 108 | t.true(router.routes['NoneProfileOrTask=Enter:Name'].enter.calledOnceWithExactly(locals, context)); 109 | }); 110 | -------------------------------------------------------------------------------- /tasker-imports/TJS_Development_Toggle.tsk.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1519560745717 4 | 1519573351396 5 | 65 6 | TJS:Development:Toggle 7 | 100 8 | 9 | 37 10 | 11 | 12 | %TJS_ENV 13 | 13 14 | development 15 | 16 | 17 | 18 | 19 | 547 20 | %TJS_ENV 21 | production 22 | 23 | 24 | 25 | 26 | 27 | 547 28 | %TJS_ENV 29 | development 30 | 31 | 32 | 33 | 34 | 35 | 548 36 | TJS: Development Mode ON 37 | 38 | Getting script from: 39 | %TJS_DEV_REMOTE 40 | 41 | 42 | 43 | 130 44 | TJS:UpdateScript 45 | 46 | %priority 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 38 55 | 56 | 57 | 547 58 | %TJS_LOCAL_PATH 59 | Documents/tasker-js-runner.js 60 | 61 | 62 | 63 | 64 | 65 | 547 66 | %TJS_DEV_REMOTE 67 | http://192.168.0.10:8080/index.js 68 | 69 | 70 | 71 | 72 | 73 | 548 74 | TJS: Initialising Tasker-JS 75 | 76 | [Remote] 77 | %TJS_DEV_REMOTE 78 | 79 | [Local] 80 | %TJS_LOCAL_PATH 81 | 82 | 83 | 84 | 130 85 | TJS:UpdateScript 86 | 87 | %priority 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 43 96 | 97 | 98 | %TJS_ENV 99 | 2 100 | development 101 | 102 | 103 | 104 | 105 | 547 106 | %TJS_ENV 107 | production 108 | 109 | 110 | 111 | 112 | 113 | 548 114 | TJS: Development Mode OFF 115 | 116 | 117 | 118 | 43 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasker JS Runner 2 | - Write Tasker task as Javascript module 3 | - Map profile name to module 4 | - Auto refresh script during development mode 5 | 6 | # Guide 7 | ## Installation 8 | with npm 9 | ``` 10 | npm install tasker-js-runner --save 11 | ``` 12 | 13 | with yarn 14 | ``` 15 | yarn add tasker-js-runner 16 | ``` 17 | ### Usage 18 | 19 | #### Defining profile map `index.js` 20 | ```javascript 21 | import TaskerJs from 'tasker-js-runner'; 22 | 23 | // Tasker Javascript modules 24 | import notification from './modules/notification'; 25 | 26 | // Construct Tasker JS and pass in mapping information as an Object 27 | new TaskerJs({ 28 | // Profile name: module 29 | 'Notification:All': notification, 30 | }); 31 | 32 | ``` 33 | 34 | #### Defining a module `modules/notification` 35 | A module should contain an `enter` and an `exit` function. 36 | The 2 functions will receive all the local variables from the profile's task 37 | via `locals` and a reference Tasker's global object via `tasker`. 38 | 39 | ```javascript 40 | export default { 41 | enter(locals, tasker) { 42 | // Example: Accessing local variables %anapp and %antitle from AutoNotification 43 | const content = locals.anapp + ' ' + locals.antitle; 44 | 45 | // Tasker's function can be accessed via the `tasker` object. 46 | tasker.setClip(content); 47 | 48 | // If you wish, you can also omit `tasker` by calling Tasker's function directly. 49 | // Behind the scene, `tasker` is mapped to the `window` object where the 50 | // Tasker's function live. 51 | flash('content'); 52 | }, 53 | 54 | exit(locals, tasker) {} 55 | }; 56 | ``` 57 | 58 | #### Asynchronous Execution 59 | 60 | All module functions are wrapped inside a Promise, which will gracefully exit at the end of execution. 61 | To ensure proper task exiting while running asynchronous code, always return a promise. 62 | ```javascript 63 | export default { 64 | enter(locals, tasker) { 65 | 66 | return new Promise((resolve, reject) => { 67 | setTimeout(() => { 68 | if (tasker.global('BLUE') === 'on') { 69 | resolve(); 70 | } else { 71 | reject(); 72 | } 73 | }, 1000); 74 | }); 75 | 76 | }, 77 | ... 78 | } 79 | ``` 80 | 81 | Otherwise, you can also permaturely terminte the script. This will immediately stop the script from executing. 82 | ```javascript 83 | export default { 84 | enter(locals, tasker) { 85 | 86 | setTimeout(() => { 87 | tasker.exit() 88 | }, 1000); 89 | 90 | }, 91 | ... 92 | } 93 | ``` 94 | 95 | ## Sample project 96 | https://github.com/amoshydra/tasker-js-runner-project 97 | 98 | ## Setup on Tasker 99 | 100 | ### Installing 101 | 1. Import the 3 tasks from the [`tasker-imports` folder](https://github.com/amoshydra/tasker-js-runner/tree/master/tasker-imports) into Tasker 102 | - `TJS_Development_Toggle.tsk.xml` 103 | - `TJS_RunScript.tsk.xml` 104 | - `TJS_UpdateScript.tsk.xml` 105 | 2. Run [TJS_Development_Toggle] inside Tasker to set up the required global variables. 106 | When [TJS_Development_Toggle] is run for the first time, it will set up all the necessary Global variables for Tasker-JS to run. 107 | - `%TJS_ENV` - Control the environment Tasker-JS-Runner to run `development`/`production`. 108 | - `%TJS_DEV_REMOTE` - The remote address where the your project script (a seperate project that make use of this library) will be downloaded. You will need to change the value of this variable in this task to match the IP of your project. 109 | - `%TJS_LOCAL_PATH` - The location where the project script will be saved (default to `Documents/tasker-js-runner.js`) 110 | 111 | ### Using 112 | 1. Create a Tasker named profile (i.e. `Notification:All`) and select `TJS:RunScript` as its task. 113 | 2. Tasker-JS-Runner will detect the profile name and execute the Javascipt module that's mapped into `Notification:All` in the Profile Map (see example from [above](#defining-profile-map-indexjs) 114 | --------------------------------------------------------------------------------