├── .DS_Store ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── README.md ├── docs ├── plugin-development.md └── todo.md ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── index.js └── util ├── config-editor.js ├── extract-metadata.js └── index.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qudo-code/state-machine-snacks/8fa4df8bd9d5353dea54807b22a8969323697210/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: ["@tivac"], 7 | parserOptions: { 8 | ecmaVersion: 2018, 9 | sourceType: "module", 10 | allowImportExportFromEverywhere: true, 11 | }, 12 | 13 | globals : { 14 | "module" : "readable", 15 | "import" : "readable", 16 | "require" : "readable", 17 | "process" : "readable", 18 | "__dirname" : "readable", 19 | "artifacts" : "readable", 20 | "TruffleContract" : "readable", 21 | "Web3" : "readable", 22 | }, 23 | 24 | plugins: ["svelte3"], 25 | overrides: [ 26 | { 27 | files: ['**/*.svelte'], 28 | processor: 'svelte3/svelte3' 29 | } 30 | ], 31 | settings: { 32 | "import/core-modules": ["svelte"] 33 | }, 34 | rules: { 35 | indent: ["error", 4], 36 | "linebreak-style": ["error", "unix"], 37 | quotes: ["error", "double"], 38 | semi: ["error", "always"], 39 | // Enforce newline consistency in objects 40 | "object-curly-newline": [ 41 | "warn", 42 | { 43 | // Object literals w/ 3+ properties need to use newlines 44 | ObjectExpression: { 45 | consistent: true, 46 | minProperties: 3 47 | }, 48 | 49 | // Destructuring w/ 6+ properties needs to use newlines 50 | ObjectPattern: { 51 | consistent: true, 52 | minProperties: 6 53 | }, 54 | 55 | // Imports w/ 4+ properties need to use newlines 56 | ImportDeclaration: { 57 | consistent: true, 58 | minProperties: 4 59 | }, 60 | 61 | // Named exports should always use newlines 62 | ExportDeclaration: "always" 63 | } 64 | ], 65 | 66 | "consistent-return": "off", 67 | 68 | "no-multiple-empty-lines": [ 69 | "warn", 70 | { 71 | max: 1, 72 | maxEOF: 1, 73 | maxBOF: 0 74 | } 75 | ], 76 | 77 | "padding-line-between-statements": [ 78 | "warn", 79 | // Always require a newline before returns 80 | { blankLine: "always", prev: "*", next: "return" }, 81 | 82 | // Always require a newline after directives 83 | { blankLine: "always", prev: "directive", next: "*" }, 84 | 85 | // Always require a newline after imports 86 | { blankLine: "always", prev: "import", next: "*" }, 87 | 88 | // Don't require a blank line between import statements 89 | { blankLine: "any", prev: "import", next: "import" }, 90 | 91 | // Newline after var blocks 92 | { blankLine: "always", prev: ["const", "let", "var"], next: "*" }, 93 | { 94 | blankLine: "any", 95 | prev: ["const", "let", "var"], 96 | next: ["const", "let", "var"] 97 | }, 98 | 99 | // Newline before conditionals/loops 100 | { 101 | blankLine: "always", 102 | prev: "*", 103 | next: ["if", "do", "while", "for"] 104 | }, 105 | 106 | // Newline after blocks 107 | { blankLine: "always", prev: "block-like", next: "*" } 108 | ], 109 | 110 | "no-restricted-syntax": [ 111 | "error", 112 | 113 | // with() 114 | "WithStatement" 115 | ], 116 | 117 | "no-restricted-globals": [ 118 | "error", 119 | { 120 | name: "isNaN", 121 | message: "isNaN is unsafe, use Number.isNaN" 122 | }, 123 | { 124 | name: "isFinite", 125 | message: "isFinite is unsafe, use Number.isFinite" 126 | } 127 | ] 128 | } 129 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | docs 4 | src 5 | .eslintrc.js 6 | .gitignore 7 | rollup.config.js -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "eslint.validate": [ 4 | "javascript", 5 | "javascriptreact", 6 | "html", 7 | "svelte" 8 | ], 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.stylelint": true, 11 | "source.fixAll.eslint": true, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ A POC, not actively maintained. 2 | 3 | # State Machine Snacks (🍕) 4 | A framework built on [XState](https://xstate.js.org/docs/about/concepts.html) that provides bite sized snacks for developing with state machine machines. 🍕 aims to increase state machine adoption in modern day web apps by providing a suite of tools and plugins to inspire development and new ways of thinking. 5 | 6 | 🐤 [@me](https://twitter.com/qudolucas) on Twitter. 7 | 8 | ### What Is XState? 9 | XState is a library that allows us to create and interpret state machines in JavaScript. It is recommended you understand the basics of XState before using State Machine UI. 10 | 11 | ## 🚀 Getting Started 12 | For basic usage, 🍕 requires only a XState state machine config as an option. SMS will utilize this config to create a machine and return an XState service. 13 | 14 | | Options | Description | | 15 | | ----------- | ----------- | ----------- | 16 | | `config` | XState state machine config. | Required 17 | | `createMachine` | By default, the machine is created with `createMachine(config)`. You can overwrite this behavior with a function that will be passed the config and must return a XState machine instance. | Optional 18 | | `interpret` | By default, the service is interpreted via `interpret(machine)`. You can overwrite this behavior with a function that will be passed both the config and machine instance from the `createMachine()` step. | Optional 19 | | `plugins` | An array of plugins you want to add to the service. | Optional 20 | 21 | #### 🍕 w/Default Settings 22 | ```javascript 23 | import sms from "state-machine-snacks"; 24 | 25 | const config = { /* ...machine config */ }; 26 | 27 | // Create your service with 🍕. 28 | const service = sms({ 29 | config, 30 | }); 31 | 32 | service.start(); 33 | ``` 34 | 35 | #### 🍕 w/Advanced Initialization 36 | ```javascript 37 | import sms from "state-machine-snacks"; 38 | 39 | const config = { /* ...machine config */ }; 40 | 41 | // Create your service with 🍕 + additional settings. 42 | const service = sms({ 43 | config, 44 | 45 | createMachine : (config) => createMachine(config, { ...actions, ...services }), 46 | 47 | interpret : (config, machine) => interpret(machine).onTransition((state) => { 48 | console.log(state.value); 49 | }); 50 | }); 51 | 52 | service.start(); 53 | ``` 54 | 55 | ## 🔌 Plugins 56 | Plugins add additional functionality to an XState config and service. 🍕 provides a plugin runner and you can add plugins to your state machine by simply adding them to the `plugins : []` option when initializing your service. 57 | 58 | - Plugins can export helper functions to be used during plugin usage and state machine composition. 59 | - Plugins are located in their own repositories prefixed with `sms-plugin---`. You can find a list of currently available plugins below. 60 | - Plugins can be passed an object containing options for the plugin. 61 | 62 | ```javascript 63 | import sms from "state-machine-snacks"; 64 | import components from "sms-plugin---components"; 65 | import logger from "sms-plugin---logger"; 66 | 67 | const config = { /* ...machine config */ }; 68 | 69 | // Create our state machine with stateUI 70 | const service = sms({ 71 | // Required 72 | config, 73 | 74 | // Example plugin usage: 75 | plugins : [ 76 | components(), 77 | logger(), 78 | ] 79 | }); 80 | 81 | service.start(); 82 | ``` 83 | 84 | ### 📦 [Plugin Components](https://github.com/qudo-lucas/sms-plugin---components) 85 | 86 | Conditionally render components as you enter/exit states. 87 | 88 | 91 | 92 | ### 📦 [Plugin Logger](https://github.com/qudo-lucas/sms-plugin---logger) 93 | 94 | Provide useful logging when developing with XState. 95 | 96 | ### [WIP] Plugin Router 97 | Map browser URLs to specific states. 98 | 99 | ## 💻 Examples 100 | ### ✨ [Simple UI](https://github.com/qudo-lucas/sms-template---simple-ui) 101 | Example of a simple UI utilizing [State Machine Snacks](https://github.com/qudo-lucas/state-machine-snacks) and [Plugin Components](https://github.com/qudo-lucas/sms-plugin---components). See how you can use a state machine to render components. 102 | 103 | ## 🛠 Contribute 104 | #### Resources 105 | - [Plugin Development](/docs/plugin-development.md) 106 | - [Todo (Project Board)](https://github.com/qudo-lucas/state-machine-snacks/projects/1) 107 | 108 | #### Links to Everything 109 | - [https://github.com/qudo-lucas/state-machine-snacks](https://github.com/qudo-lucas/state-machine-snacks) 110 | - [https://github.com/qudo-lucas/sms-plugin---logger](https://github.com/qudo-lucas/sms-plugin---logger) 111 | - [https://github.com/qudo-lucas/sms-plugin---components](https://github.com/qudo-lucas/sms-plugin---components) 112 | - [https://github.com/qudo-lucas/sms-template---simple-ui](https://github.com/qudo-lucas/sms-template---simple-ui) 113 | - [https://github.com/qudo-lucas/sms-template---plugin](https://github.com/qudo-lucas/sms-template---plugin) 114 | -------------------------------------------------------------------------------- /docs/plugin-development.md: -------------------------------------------------------------------------------- 1 | [⬅ Back to 🍕](https://github.com/qudo-lucas/state-machine-snacks) 2 | # Plugin Development 3 | 4 | [👨🏽‍💻 Plugin Developer Template](https://github.com/qudo-lucas/sms-template---plugin) 5 | 6 | ### Dev Helpers 7 | There are helper functions located available via 🍕 that you should utilize throughout plugin development. 8 | ```javascript 9 | import { util } from "state-machine-snacks"; 10 | 11 | const { 12 | configEditor, 13 | extractMetadata, 14 | } = util; 15 | ``` 16 | | Helper | Description | Function | 17 | | ------ | ------ | ---- | 18 | | [Config Editor](#config-editor) | Useful during the `config` hook. | `configEditor` | 19 | | [Extract Metadata From State Chart](#extract-metadata) | Generates a map of states that have matadata. | `extractMetadata` | 20 | 21 | ## Config Editor 22 | Append things like events, context, and states to a users config without affecting any original values. 23 | 24 | **Note:** These helpers can only be used in the `config` hook during the plugin lifecycle. 25 | 26 | ```javascript 27 | import { assign } from "xstate"; 28 | import { util } from "state-machine-snacks"; 29 | 30 | const { 31 | configEditor, 32 | } = util; 33 | 34 | export default () => { 35 | config : (config) => { 36 | let result = { ...config }; 37 | 38 | // Add context, a place where we can store values that the user can also read. 39 | result = configEditor.addContext(result, { someContext : "some value" }) 40 | 41 | 42 | // Add an update event used to update context. 43 | result = configEditor.addEventListener(result, { plugin:myPluginName:UPDATE_STATE : { 44 | actions : assign({ 45 | someContext : (ctx, event) => event.data, 46 | }) 47 | }}) 48 | 49 | // Add a new state. 50 | result = configEditor.addState(result, { 51 | coolState : { 52 | entry : () => console.log("cool state bro) 53 | }, 54 | }) 55 | 56 | return result; 57 | }, 58 | }) 59 | ``` 60 | 61 | ### Adding Events 62 | `configEditor.addEventListener(config, event)` 63 | You can add events to the users config during the `config` hook with the `addEventListener` function. 64 | 65 | When adding event listeners, it is recommended that you prefix events with a pattern similar to the following example. 66 | 67 | **Example:** ```{ plugin:yourPluginName:WHATEVER_YOU_WANT : ".stateTwo" }``` 68 | | Args | Description | | 69 | | ----------- | ----------- | ----------- | 70 | | config | XState state machine config. | Required 71 | | event | XState event object. | Required 72 | 73 | ### Adding Context 74 | `configEditor.addContext(config, context)` 75 | You can add context to the users config during the `config` hook with the `addContext` function. 76 | 77 | 78 | **Example:** ```{ myPluginsContext : "some values for my plugin or the user" }``` 79 | | Args | Description | | 80 | | ----------- | ----------- | ----------- | 81 | | config | XState state machine config. | Required 82 | | context | Object to be appended to machine context. | Required 83 | 84 | ### Adding States 85 | `configEditor.addState(config, context)` 86 | You can add states to the users config during the `config` hook with the `addState` function. 87 | 88 | **Example:** ```{ myPluginsState : { entry : () => console.log("made it")}}``` 89 | | Args | Description | | 90 | | ----------- | ----------- | ----------- | 91 | | config | XState state machine config. | Required 92 | | state | Object to be appended to machine states. | Required 93 | 94 |

Extract Metadata

95 | 96 | Extract metadata from a service. Often times plugins require user input provided via metadata in the state chart config. This function accepts a service, plus a list of metadata keys you wish to extract. 97 | 98 | This would build a map of all state machine states that have metatadata property "component". 99 | ```javascript 100 | const componentsMap = extractMetadataFromState(service, [ "component" ]); 101 | 102 | // Output: 103 | // Map([ 104 | // [ 105 | // "exampleState.anotherState", 106 | // metadataValue 107 | // ], 108 | // [ 109 | // "someStateWithMeta", 110 | // otherMetadataValue 111 | // ] 112 | // ]) 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | ## Tasks 4 | - [ ] Make this a project board. 5 | - [ ] Build mock project for dev env. 6 | - Maybe the dev environment can also just be the demo project? 7 | - [ ] Test components plugin IRL. 8 | - [ ] Test this whole thing IRL. 9 | - [ ] Add dev documentation. 10 | - [ ] Convert to TS. 11 | - [ ] Write tests. 12 | - [ ] Create at least one demo project before release (Simple UI). 13 | 14 | ## Demo Project Ideas 15 | ### Simple UI 16 | A starter template for UI development. Something like Uniswap. Should use the components plugin and the router. 17 | 18 | ### Ethereum Blockchain 19 | Template for interacting with the Ethereum blockchain. 20 | 21 | ### Solana Blockchain 22 | Template for interacting with the Solana blockchain. 23 | 24 | ### Video Call 25 | Template for initiating peer to peer video calls via WebRTC + Firebase. 26 | 27 | ### WSIO P2P Chat 28 | Template for initiating peer to peer text chats. Firebase? 29 | 30 | ## Plugin Ideas 31 | 32 | ### Cleaner State Manangement 33 | Make app state managment in context feel better. 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "state-machine-snacks", 3 | "version": "1.0.4", 4 | "description": "A framework built on XState that provides useful state machine plugins and patterns geared toward UI development.", 5 | "main": "./dist/lib/index.js", 6 | "module": "./dist/lib/index.mjs", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "rollup -c", 10 | "autobuild": "rollup -c -w", 11 | "dev": "npm run autobuild" 12 | }, 13 | "author": "State Machine Snacks", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.10.5", 17 | "@rollup/plugin-commonjs": "^12.0.0", 18 | "@rollup/plugin-node-resolve": "^7.1.3", 19 | "@tivac/eslint-config": "^2.4.0", 20 | "eslint": "^6.8.0", 21 | "eslint-plugin-import": "^2.20.2", 22 | "eslint-plugin-svelte3": "^2.7.3", 23 | "rollup": "^2.22.1", 24 | "rollup-plugin-terser": "^7.0.2", 25 | "xstate": "^4.20.0" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/qudo-lucas/state-machine-snacks.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/qudo-lucas/state-machine-snacks" 33 | }, 34 | "homepage": "https://github.com/qudo-lucas/state-machine-snacks" 35 | } 36 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import { terser } from "rollup-plugin-terser"; 4 | 5 | const inputDir = "src"; 6 | 7 | const { 8 | name, 9 | main, 10 | module : esm, 11 | } = require("./package.json"); 12 | 13 | export default { 14 | input : `${inputDir}/index.js`, 15 | output : [ 16 | { 17 | file : esm, 18 | format : "es", 19 | }, 20 | { 21 | file : main, 22 | format : "cjs", 23 | name, 24 | }, 25 | ], 26 | plugins : [ 27 | resolve({ 28 | browser : false, 29 | }), 30 | commonjs(), 31 | terser(), 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-statements */ 2 | import { createMachine, interpret } from "xstate"; 3 | 4 | import util from "./util/index.js"; 5 | 6 | const { 7 | name : packageName, 8 | } = require("./package.json"); 9 | 10 | const error = () => console.error(`${packageName}: ${error}`); 11 | 12 | export default ({ 13 | config : existingConfig = false, 14 | createMachine : customCreateMachine = false, 15 | interpret : customInterpret = false, 16 | plugins = [], 17 | }) => { 18 | try { 19 | if(!existingConfig) { 20 | error("No machine config provided."); 21 | } 22 | 23 | // Pull off and sort plugin hooks 24 | const configModifiers = []; 25 | const serviceModifiers = []; 26 | 27 | plugins.forEach(({ 28 | config : configMod = false, 29 | service : serviceMod = false, 30 | }) => { 31 | if(configMod) { 32 | configModifiers.push(configMod); 33 | } 34 | 35 | if(serviceMod) { 36 | serviceModifiers.push(serviceMod); 37 | } 38 | }); 39 | 40 | let config = existingConfig; 41 | let machine = false; 42 | let service = false; 43 | 44 | // Modify config via "config" plugin hooks 45 | configModifiers.forEach((configMod) => config = configMod(config)); 46 | 47 | // Use custom machine creation if provided. 48 | if(customCreateMachine) { 49 | machine = customCreateMachine(config); 50 | } else { 51 | // Fallback to simple machine creation. 52 | machine = createMachine(config); 53 | } 54 | 55 | // Use custom machine interpreter if provided. 56 | if(customInterpret) { 57 | service = customInterpret(machine); 58 | } else { 59 | // Fallback to simple interpret. 60 | service = interpret(machine); 61 | } 62 | 63 | serviceModifiers.forEach((serviceMod) => serviceMod(config, service)); 64 | 65 | return service; 66 | } catch(err) { 67 | error(err); 68 | } 69 | }; 70 | 71 | export { 72 | util, 73 | }; 74 | -------------------------------------------------------------------------------- /src/util/config-editor.js: -------------------------------------------------------------------------------- 1 | // Utilities for editing a machine config. 2 | export default { 3 | addEventListener : (config, event = {}) => ({ 4 | ...config, 5 | 6 | on : { 7 | ...config.on || {}, 8 | ...event, 9 | }, 10 | }), 11 | 12 | addContext : (config, context = {}) => ({ 13 | ...config, 14 | 15 | context : { 16 | ...config.context || {}, 17 | ...context, 18 | }, 19 | }), 20 | 21 | addState : (config, state = {}) => ({ 22 | ...config, 23 | 24 | state : { 25 | ...config.state || {}, 26 | ...state, 27 | }, 28 | }), 29 | }; 30 | -------------------------------------------------------------------------------- /src/util/extract-metadata.js: -------------------------------------------------------------------------------- 1 | // Extract Metadata From Service 2 | // Provide a service and an array of keys as strings. 3 | // This function will loop over every state in your config and look for states that contain meta data that matches the keys provided. 4 | 5 | // Example Usage: 6 | // const componentsMap = extractMetadataFromState(service, [ "component", "props" ]); 7 | 8 | // The above example would return a map of states that have meta data containing keys "component" or "props". 9 | export default (service, metaKeys) => { 10 | const { idMap : ids } = service.machine; 11 | 12 | const map = Object.entries(ids).reduce((acc, [ id, val ]) => { 13 | // See if state has metadata 14 | if(val.meta) { 15 | // Loop over provided keys to extract and add them to the map if they exist. 16 | metaKeys.forEach((key) => { 17 | if(val.meta[key]) { 18 | acc.set(id.replace("(machine).", ""), val); 19 | } 20 | }); 21 | } 22 | 23 | return acc; 24 | }, new Map()); 25 | 26 | return map; 27 | }; 28 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import configEditor from "./config-editor.js"; 2 | import extractMetadata from "./extract-metadata.js"; 3 | 4 | export default { 5 | configEditor, 6 | extractMetadata, 7 | }; 8 | --------------------------------------------------------------------------------