Comparing version 0.3.0 to 0.4.0
@@ -0,1 +1,3 @@ | ||
Copyright © 2018-present Wolfgang | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
@@ -17,2 +19,2 @@ of this software and associated documentation files (the "Software"), to deal | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
SOFTWARE. |
{ | ||
"name": "fxapp", | ||
"description": "Build JavaScript apps using effects as data", | ||
"version": "0.3.0", | ||
"main": "dist/fxapp.js", | ||
"module": "src/index.js", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/fxapp/fxapp.git" | ||
}, | ||
"description": "Build JavaScript server apps using effects as data", | ||
"version": "0.4.0", | ||
"main": "./src/index.js", | ||
"files": [ | ||
"src", | ||
"dist" | ||
"src" | ||
], | ||
"keywords": [ | ||
"effects", | ||
"data", | ||
"vdom" | ||
], | ||
"devDependencies": { | ||
"babel-preset-env": "=1.6.1", | ||
"eslint": "=4.19.1", | ||
"eslint-plugin-compat": "=2.2.0", | ||
"jest": "=22.4.3", | ||
"rollup": "=0.57.1", | ||
"uglify-js": "=3.3.16" | ||
"eslint": "=6.7.2", | ||
"jest": "=24.9.0", | ||
"nodemon": "=2.0.2", | ||
"prettier": "=1.19.1" | ||
}, | ||
"scripts": { | ||
"clean": "npx rimraf coverage dist node_modules", | ||
"format": "npx prettier --write {src,test}/**/*.js", | ||
"format:check": "npx prettier --list-different {src,test}/**/*.js", | ||
"lint": "eslint {src,test}/**/*.js", | ||
"test": "jest --coverage --no-cache", | ||
"bundle": "rollup -i src/index.js -o dist/fxapp.js -m -f umd -n fx", | ||
"minify": "uglifyjs dist/fxapp.js -o dist/fxapp.js -mc --source-map includeSources,url=fxapp.js.map", | ||
"format": "prettier --ignore-path .gitignore --write \"**/*.js\"", | ||
"format:check": "prettier --ignore-path .gitignore --list-different \"**/*.js\"", | ||
"lint": "eslint .", | ||
"test": "jest --coverage", | ||
"start": "nodemon --ext js example", | ||
"check": "npm run format:check && npm run lint && npm t", | ||
"build": "npm run check && npm run bundle && npm run minify", | ||
"prepare": "npm run build", | ||
"prepare": "npm run check", | ||
"release": "./pre-flight-tests && npm run clean && git pull && npm i && git tag $npm_package_version && git push && git push --tags && npm publish" | ||
}, | ||
"babel": { | ||
"presets": "env" | ||
}, | ||
"eslintConfig": { | ||
"extends": "eslint:recommended", | ||
"plugins": [ | ||
"compat" | ||
], | ||
"parserOptions": { | ||
"sourceType": "module" | ||
}, | ||
"env": { | ||
"browser": true | ||
}, | ||
"rules": { | ||
"no-use-before-define": "error", | ||
"compat/compat": "error" | ||
"node": true, | ||
"es6": true | ||
} | ||
}, | ||
"browserslist": [ | ||
"IE 10" | ||
], | ||
"jest": { | ||
"collectCoverageFrom": [ | ||
"src/**/*.js" | ||
] | ||
}, | ||
"author": "Wolfgang Wedemeyer <wolf@okwolf.com>", | ||
"license": "MIT", | ||
"repository": "fxapp/fxapp", | ||
"bugs": { | ||
@@ -66,0 +42,0 @@ "url": "https://github.com/fxapp/fxapp/issues" |
251
README.md
@@ -7,24 +7,245 @@ # FX App | ||
Here is an example counter that can be incremented or decremented. Go ahead and [try it online](https://codepen.io/okwolf/pen/WMWBjR?editors=0010). | ||
Build JavaScript server apps using [_effects as data_](https://youtu.be/6EdXaWfoslc). Requests and responses are represented as data and FX use this data to interact with the imperative [IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage) and [ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) APIs provided by Node. | ||
## Getting Started | ||
```console | ||
$ npm i fxapp | ||
``` | ||
```js | ||
fx.app({ | ||
state: { | ||
count: 0 | ||
}, | ||
actions: { | ||
down: ({ state, fx }) => fx.merge({ count: state.count - 1 }), | ||
up: ({ state, fx }) => fx.merge({ count: state.count + 1 }) | ||
}, | ||
view: ({ state, fx }) => [ | ||
"main", | ||
["h1", state.count], | ||
["button", { onclick: fx.action("down") }, "-"], | ||
["button", { onclick: fx.action("up") }, "+"] | ||
] | ||
const { app } = require("fxapp"); | ||
app({ | ||
routes: () => ({ | ||
response: { | ||
text: "Hello World" | ||
} | ||
}) | ||
}); | ||
``` | ||
[http://localhost:8080](http://localhost:8080) | ||
## `app` Options | ||
### `port` | ||
Default: `8080` | ||
The port on which the server will listen. | ||
### `initFx` | ||
Optional initial [`dispatch`able(s)](#dispatchable-types) that are run on server start before accepting any requests. Use this to set initial global state or for side effectful initialization like opening required resources or network connections. | ||
### `requestFx` | ||
Optional [`dispatch`able(s)](#dispatchable-types) that are run on every request before the router. Use this for parsing custom request data, custom routing, sending [custom responses](#responsecustom), or side FX like logging. | ||
### `routes` | ||
Default: `{}` | ||
Routes are defined as a nested object structure with some properties having special meanings. The first matching route value will be dispatched. | ||
Example: | ||
```js | ||
app({ | ||
routes: { | ||
// GET /unknown/path | ||
_: fallbackAction, | ||
path: { | ||
some: { | ||
// GET /path/some | ||
GET: someReadAction, | ||
// POST /path/some | ||
POST: someAddAction | ||
}, | ||
other: { | ||
// GET /path/other/123 | ||
$id: otherAction | ||
} | ||
} | ||
} | ||
}); | ||
``` | ||
#### Default Routes | ||
The special wildcard `_` route is reserved for routes that match in the absence of a more specific route. Useful for 404 and related behaviors. Sending a `GET` request to `/unknown/path` will respond with the results of `fallbackAction`. | ||
#### HTTP Method Routes | ||
Routes with the name of an [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods) will match any requests with that method. Sending a `GET` request to `/path/some` with the example route will respond with the results of `someReadAction`. A `POST` request to `/path/some` will respond with the results of `someAddAction`. | ||
#### Path Params | ||
Routes beginning with `$` are reserved and define a path parameter for matching at that position. In the example above sending a `GET` request to `/path/other/123` will respond with the results of passing `{id: "123"}` as the `request.params` to `otherAction`. | ||
## State Shape | ||
```js | ||
{ | ||
request: { | ||
method: "GET", | ||
url: "/path/other/123?param=value&multiple=1&multiple=2", | ||
path: "/path/other/123", | ||
query: { param: "value", multiple: ["1", "2"] }, | ||
params: { id: "123" }, | ||
headers: { | ||
Host: "localhost:8080" | ||
} | ||
}, | ||
response: { | ||
statusCode: 200, | ||
headers: { Server: "fxapp" }, | ||
text: "Hello World" | ||
} | ||
} | ||
``` | ||
### `request` | ||
Normalized data parsed from the [HTTP request](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_message) that is currently being processed. | ||
#### `request.method` | ||
Examples: `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH` | ||
The [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods) used to make the request. | ||
#### `request.url` | ||
The full request URL, including query parameters. | ||
#### `request.path` | ||
The request path, omitting query parameters. | ||
#### `request.query` | ||
An object containing the request query parameters. Multiple instances of the same parameter are stored as an array. | ||
#### `request.params` | ||
An object containing path parameters from the router. | ||
#### `request.headers` | ||
An object containing all [HTTP request headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Standard_request_fields). | ||
#### `request.body` | ||
The contents of the request body. Respects the [`Content-Length`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) header. | ||
#### `request.jsonBody` | ||
This will be set if the `Content-Type` of the request is `application/json` and the body content is valid JSON. | ||
### `response` | ||
Data representing the [HTTP response](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Response_message) that will be sent to the client once all FX are done running. | ||
#### `response.statusCode` | ||
Default: `200` | ||
The [HTTP status code](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes) to send in the response. | ||
#### `response.headers` | ||
The [HTTP response headers](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields) to send. | ||
#### `response.custom` | ||
Skip the default logic for sending the response body. Make sure you provide [`requestFx`](#requestfx) to handle this response or the request will hang for the client. | ||
#### `response.json` | ||
The HTTP response body that will be sent as `application/json`. Value will be formatted using `JSON.stringify`. | ||
#### `response.html` | ||
The HTTP response body that will be sent as `text/html`. | ||
#### `response.filePath` | ||
The HTTP response will pipe the contents of the file at the given path. Never pass user-provided data for this as that would introduce a vulnerability for arbitrary disk access. You may need to add `response.contentType` in order for the client to interpret the response correctly. | ||
#### `response.text` | ||
The HTTP response body that will be sent as `text/plain` unless a value is passed for `response.contentType`. | ||
## Dispatchable Types | ||
```js | ||
StateUpdate = function(state: Object) => newState: Object | ||
ReservedProps = { | ||
concurrent: boolean? = false, | ||
after: boolean? = false, | ||
cancel: boolean? = false | ||
} | ||
FX = { | ||
run: ({ | ||
dispatch: function(Dispatchable), | ||
serverRequest: IncomingMessage, | ||
serverResponse: ServerResponse, | ||
...ReservedProps, | ||
// Additional props | ||
}) => Promise? | undefined, | ||
...ReservedProps, | ||
// Additional props | ||
} | ||
Dispatchable = StateUpdate | FX | [Dispatchable] | ||
``` | ||
### State Mapping Function `state => newState` | ||
Perform an immutable state update by receiving the current state as a parameter and returning the new state. Automatically shallow merges root properties in addition to one level under `request` and `response`. | ||
### FX | ||
_FX as data_ are represented with an object containing a `run` function and additional properties that will be passed to that function. | ||
The `run` function returns a `Promise` if the effect is async. Async FX are considered still running until resolved or rejected. Otherwise FX are considered sync and done once the `run` function returns. | ||
Some `props` are reserved and have special meaning: | ||
#### `concurrent` | ||
Default: `false` | ||
Used for running multiple FX in parallel where the results are unrelated. These FX will take priority and must all complete before running any nonconcurrent FX. | ||
#### `after` | ||
Default: `false` | ||
Run FX after all others are complete. Use this for logging, cleanup, or providing custom response-sending logic. | ||
#### `cancel` | ||
Default: `false` | ||
Cancel all other FX immediately. Cancelled FX are no longer able to dispatch. FX already dispatched with `after` will still be run to allow for the response to be sent. Use this to enforce response timeouts or for handling errors. | ||
#### `serverRequest` | ||
The internal [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage) used by the Node HTTP server implementation. Allows for FX to interact with the request object to get additional data. | ||
#### `serverResponse` | ||
The internal [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) used by the Node HTTP server implementation. Allows for FX to send other types of responses. | ||
### Arrays | ||
A batch of state mapping functions and/or FX may be dispatched by wrapping them in an array. | ||
## License | ||
FX App is MIT licensed. See [LICENSE](LICENSE.md). |
@@ -1,52 +0,42 @@ | ||
import { isFn, assign, get } from "./utils"; | ||
import { makeFx } from "./fxUtils"; | ||
import { patch } from "./patch"; | ||
const { createServer } = require("http"); | ||
const { assign } = require("./utils"); | ||
const makeServer = require("./makeServer"); | ||
const makeServerRuntime = require("./makeServerRuntime"); | ||
const parseRequest = require("./fx/parseRequest"); | ||
const parseBody = require("./fx/parseBody"); | ||
const sendResponse = require("./fx/sendResponse"); | ||
const makeRouter = require("./fx/makeRouter"); | ||
export function app(props) { | ||
var store = { | ||
state: assign(props.state), | ||
actions: assign(props.actions) | ||
}; | ||
function wireFx(namespace, state, actions) { | ||
var sliceFx = makeFx(namespace, store, props.fx); | ||
for (var key in actions) { | ||
isFn(actions[key]) | ||
? (function(key, action) { | ||
actions[key] = function(data) { | ||
var actionResult = action({ | ||
state: get(namespace, store.state), | ||
data: data, | ||
fx: sliceFx.creators | ||
}); | ||
sliceFx.run(actionResult); | ||
return actionResult; | ||
}; | ||
})(key, actions[key]) | ||
: wireFx( | ||
namespace.concat(key), | ||
(state[key] = assign(state[key])), | ||
(actions[key] = assign(actions[key])) | ||
); | ||
} | ||
} | ||
wireFx([], store.state, store.actions); | ||
var rootFx = makeFx([], store, props.fx); | ||
var container = props.container || document.body; | ||
function render() { | ||
var nextNode = props.view({ | ||
state: store.state, | ||
fx: rootFx.creators | ||
}); | ||
patch(nextNode, container, rootFx); | ||
} | ||
if (isFn(props.view)) { | ||
store.onchange = render; | ||
render(); | ||
} | ||
return store.actions; | ||
} | ||
module.exports = options => { | ||
const mergedOptions = assign( | ||
{ | ||
port: 8080, | ||
httpApi: createServer, | ||
makeServer, | ||
makeServerRuntime, | ||
parseRequest, | ||
parseBody, | ||
sendResponse, | ||
makeRouter | ||
}, | ||
options | ||
); | ||
return mergedOptions | ||
.makeServerRuntime({ | ||
initFx: options.initFx, | ||
requestFx: [ | ||
mergedOptions.parseRequest, | ||
mergedOptions.parseBody, | ||
options.requestFx, | ||
mergedOptions.makeRouter(options.routes), | ||
mergedOptions.sendResponse | ||
] | ||
}) | ||
.then(serverRuntime => | ||
mergedOptions.makeServer({ | ||
port: mergedOptions.port, | ||
httpApi: mergedOptions.httpApi, | ||
serverRuntime | ||
}) | ||
); | ||
}; |
@@ -1,2 +0,3 @@ | ||
export { h } from "./h"; | ||
export { app } from "./app"; | ||
const app = require("./app"); | ||
module.exports = { app }; |
@@ -1,45 +0,14 @@ | ||
export var isArray = Array.isArray; | ||
const isArray = Array.isArray; | ||
const isFn = value => typeof value === "function"; | ||
const isObj = value => value && typeof value === "object" && !isArray(value); | ||
const isFx = value => isObj(value) && isFn(value.run); | ||
export function isFn(value) { | ||
return typeof value === "function"; | ||
} | ||
const assign = (...args) => Object.assign({}, ...args); | ||
export function isObj(value) { | ||
return typeof value === "object" && !isArray(value); | ||
} | ||
export function assign(from, assignments) { | ||
var i, | ||
obj = {}; | ||
for (i in from) obj[i] = from[i]; | ||
for (i in assignments) obj[i] = assignments[i]; | ||
return obj; | ||
} | ||
export function set(prefixes, value, from) { | ||
var target = {}; | ||
if (prefixes.length) { | ||
target[prefixes[0]] = | ||
prefixes.length > 1 | ||
? set(prefixes.slice(1), value, from[prefixes[0]]) | ||
: value; | ||
return assign(from, target); | ||
} | ||
return value; | ||
} | ||
export function get(prefixes, from) { | ||
for (var i = 0; i < prefixes.length; i++) { | ||
from = from && from[prefixes[i]]; | ||
} | ||
return from; | ||
} | ||
export function reduceByNameAndProp(items, prop) { | ||
return items.reduce(function(others, next) { | ||
others[next.name] = next[prop]; | ||
return others; | ||
}, {}); | ||
} | ||
module.exports = { | ||
isArray, | ||
isFn, | ||
isObj, | ||
isFx, | ||
assign | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
4
13
353
251
21141
2
2