react-machine
Advanced tools
| # Changelog | ||
| ## 0.2.0 | ||
| First release 🎉. Thanks to @tempname11 for the npm package name. |
+378
| const hookKeys = { | ||
| guard: 'guards', | ||
| reduce: 'reducers', | ||
| effect: 'effects', | ||
| invoke: 'invokes', | ||
| } | ||
| const transitionHooks = ['assign', 'reduce', 'action', 'guard'] | ||
| const enterHooks = ['assign', 'reduce', 'action', 'invoke', 'effect'] | ||
| const exitHooks = ['assign', 'reduce', 'action'] | ||
| const mappedHooks = { | ||
| assign: ['reduce', assignToReduce], | ||
| action: ['reduce', actionToReduce], | ||
| } | ||
| const ACTION = {} | ||
| const env = (process && process.env && process.env.NODE_ENV) || 'development' | ||
| function warn(msg) { | ||
| if (env !== 'production') { | ||
| console.warn(msg) | ||
| } | ||
| } | ||
| function arg(argument, type, error) { | ||
| if (type === 'string') { | ||
| if (typeof argument !== 'string') { | ||
| throw new Error(error) | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Parse the machine DSL into a machine object. | ||
| */ | ||
| export function createMachine(create) { | ||
| const machine = { states: {} } | ||
| if (create) { | ||
| create({ | ||
| state: (name, ...opts) => { | ||
| machine.states[name] = createState(name, ...opts) | ||
| }, | ||
| enter: createEnter, | ||
| exit: createExit, | ||
| transition: createTransition, | ||
| immediate: createImmediate, | ||
| internal: createInternal, | ||
| }) | ||
| } | ||
| validate(machine) | ||
| return machine | ||
| } | ||
| function validate(machine) { | ||
| for (const [, state] of Object.entries(machine.states)) { | ||
| for (const transition of state.immediates) { | ||
| if (!machine.states[transition.target]) { | ||
| throw new Error(`Invalid transition target '${transition.target}'`) | ||
| } | ||
| } | ||
| for (const transitions of Object.values(state.transitions)) { | ||
| for (const transition of transitions) { | ||
| if (!transition.internal && !machine.states[transition.target]) { | ||
| throw new Error(`Invalid transition target '${transition.target}'`) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Create a state node. | ||
| */ | ||
| function createState(name, ...opts) { | ||
| const enter = [] | ||
| const exit = [] | ||
| const transitions = {} | ||
| const immediates = [] | ||
| for (const opt of opts) { | ||
| const { type, event } = opt | ||
| if (type === 'transition') { | ||
| if (!transitions[event]) transitions[event] = [] | ||
| transitions[event].push(opt) | ||
| } else if (type === 'immediate') { | ||
| immediates.push(opt) | ||
| } else if (type === 'enter') { | ||
| enter.push(opt) | ||
| } else if (type === 'exit') { | ||
| exit.push(opt) | ||
| } else { | ||
| throw new Error( | ||
| `State '${name}' should be passed one of enter(), exit(), transition(), immediate() or internal()` | ||
| ) | ||
| } | ||
| } | ||
| return { | ||
| name, | ||
| enter, | ||
| exit, | ||
| transitions, | ||
| immediates, | ||
| } | ||
| } | ||
| function createEnter(opts) { | ||
| return { type: 'enter', ...merge(opts, enterHooks) } | ||
| } | ||
| function createExit(opts) { | ||
| return { type: 'exit', ...merge(opts, exitHooks) } | ||
| } | ||
| function createTransition(event, target, opts) { | ||
| arg(event, 'string', 'First argument of the transition must be the name of the event') | ||
| arg(target, 'string', 'Second argument of the transition must be the name of the target state') | ||
| return { type: 'transition', event, target, ...merge(opts, transitionHooks) } | ||
| } | ||
| function createInternal(event, opts) { | ||
| arg(event, 'string', 'First argument of the internal transition must be the name of the event') | ||
| return { | ||
| type: 'transition', | ||
| event, | ||
| internal: true, | ||
| ...merge(opts, transitionHooks), | ||
| } | ||
| } | ||
| function createImmediate(target, opts) { | ||
| arg( | ||
| target, | ||
| 'string', | ||
| 'First argument of the immediate transition must be the name of the target state' | ||
| ) | ||
| return { type: 'immediate', target, ...merge(opts, transitionHooks) } | ||
| } | ||
| /** | ||
| * Transition the given machine, with the given state | ||
| * to the next state based on the event. Returns the tuple | ||
| * of the next state and any events to execute. In case no external | ||
| * transition took place, return null as effects, to indicate | ||
| * that the active effects should continue running. | ||
| */ | ||
| export function transition(machine = {}, state = {}, event) { | ||
| event = typeof event === 'string' ? { type: event } : event | ||
| // initial transition | ||
| if (!state.name && event && event.type === null) { | ||
| const stateNames = Object.keys(machine.states) | ||
| if (stateNames.length > 0) { | ||
| const initialStateName = stateNames[0] | ||
| const initialTransition = createImmediate(initialStateName) | ||
| return applyTransition(machine, state, event, initialTransition) | ||
| } | ||
| } | ||
| const currState = machine.states[state.name] || {} | ||
| const transitions = currState.transitions || {} | ||
| const candidates = transitions[event.type] || [] | ||
| for (const candidate of candidates) { | ||
| if (checkGuards(state.context, event, candidate)) { | ||
| return applyTransition(machine, state, event, candidate) | ||
| } | ||
| } | ||
| return [state, null] | ||
| } | ||
| /** | ||
| * The logic of applying a transition to the machine. Exit states, | ||
| * apply transition hooks, enter states and collect any events. Do this | ||
| * recursively untill all immediate transitions settle. | ||
| */ | ||
| function applyTransition(machine, curr, event, transition) { | ||
| const next = { ...curr } | ||
| const target = transition.internal ? curr.name : transition.target | ||
| const currState = machine.states[curr.name] | ||
| const nextState = machine.states[target] | ||
| const effects = transition.internal ? null : [] | ||
| if (currState && !transition.internal) { | ||
| for (const exit of currState.exit) { | ||
| applyReducers(next, event, exit.reducers) | ||
| } | ||
| } | ||
| next.name = target | ||
| applyReducers(next, event, transition.reducers) | ||
| if (!transition.internal) { | ||
| for (const enter of nextState.enter) { | ||
| applyReducers(next, event, enter.reducers) | ||
| } | ||
| } | ||
| for (const candidate of nextState.immediates) { | ||
| if (checkGuards(next.context, event, candidate)) { | ||
| return applyTransition(machine, next, event, candidate) | ||
| } | ||
| } | ||
| if (Object.keys(nextState.transitions).length === 0 && nextState.immediates.length === 0) { | ||
| next.final = true | ||
| } | ||
| if (!transition.internal) { | ||
| for (const enter of nextState.enter) { | ||
| for (const invoke of enter.invokes) { | ||
| effects.push({ run: promiseEffect(invoke), event }) | ||
| } | ||
| for (const effect of enter.effects) { | ||
| effects.push({ run: effect, event }) | ||
| } | ||
| } | ||
| } | ||
| return [next, effects] | ||
| } | ||
| function checkGuards(context, event, transition) { | ||
| return !transition.guards.length || transition.guards.every((g) => g(context, event)) | ||
| } | ||
| function applyReducers(next, event, reducers) { | ||
| for (const reduce of reducers) { | ||
| const result = reduce(next.context, event) | ||
| if (result !== ACTION) { | ||
| next.context = result | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * A common operation is to assign event payload | ||
| * to the context, this allows to do in several ways: | ||
| * true - assign the full event payload to context | ||
| * fn - assign the result of the fn(context, data) to context | ||
| * val - assign the constant provided value to context | ||
| */ | ||
| function assignToReduce(assign) { | ||
| return (context, event) => { | ||
| const { type, ...data } = event | ||
| if (assign === true) { | ||
| return { ...context, ...data } | ||
| } | ||
| if (typeof assign === 'function') { | ||
| return { ...context, ...assign(context, data) } | ||
| } | ||
| return { ...context, ...assign } | ||
| } | ||
| } | ||
| function actionToReduce(action) { | ||
| return (context, event) => { | ||
| action(context, event) | ||
| return ACTION | ||
| } | ||
| } | ||
| /** | ||
| * Allow to pass each hook as a function, or a list of functions | ||
| * transition(..., { reduce: fn }) | ||
| * transition(..., { reduce: [fn1, fn2] }) | ||
| * Convert both of those into arrays, and also remap some of the | ||
| * hooks to different hooks (i.e. assign -> reduce) | ||
| */ | ||
| function merge(opts = {}, allowedHooks) { | ||
| const merged = {} | ||
| for (const hook of allowedHooks) { | ||
| add(hook) | ||
| } | ||
| function add(hook) { | ||
| let t = opts[hook] || [] | ||
| t = Array.isArray(t) ? t : [t] | ||
| if (mappedHooks[hook]) { | ||
| const [newName, transform] = mappedHooks[hook] | ||
| hook = newName | ||
| t = t.map(transform) | ||
| } | ||
| const key = hookKeys[hook] | ||
| merged[key] = merged[key] || [] | ||
| merged[key] = merged[key].concat(t) | ||
| } | ||
| return merged | ||
| } | ||
| /** | ||
| * Convert an async function into an effect | ||
| * that sends 'done' and 'error' events | ||
| */ | ||
| function promiseEffect(fn) { | ||
| return (curr, event, send) => { | ||
| let disposed = false | ||
| Promise.resolve(fn(curr, event)) | ||
| .then((data) => { | ||
| if (!disposed) { | ||
| send({ type: 'done', data }) | ||
| } | ||
| }) | ||
| .catch((error) => { | ||
| if (!disposed) { | ||
| send({ type: 'error', error }) | ||
| } | ||
| }) | ||
| return () => { | ||
| disposed = true | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * createMachine and transition are pure, stateless functions. After | ||
| * transitioning the machine to the next state, the caller must clean | ||
| * up all of the running effects, and then run the newly provided effects. | ||
| */ | ||
| export function runEffects(effects = [], state, send) { | ||
| const runningEffects = [] | ||
| for (const effect of effects) { | ||
| const safeSend = (...args) => { | ||
| if (effect.disposed) { | ||
| warn( | ||
| [ | ||
| "Can't send events in an effect after it has been cleaned up.", | ||
| 'This is a no-op, but indicates a memory leak in your application.', | ||
| "To fix, cancel all subscriptions and asynchronous tasks in the effect's cleanup function.", | ||
| ].join(' ') | ||
| ) | ||
| } else { | ||
| return send(...args) | ||
| } | ||
| } | ||
| const dispose = effect.run(state.context, effect.event, safeSend) | ||
| if (dispose && dispose.then) { | ||
| warn( | ||
| [ | ||
| 'Effect function must return a cleanup function or nothing.', | ||
| 'Use invoke instead of effect for async functions, or call the async function inside the synchronous effect function.', | ||
| ].join(' ') | ||
| ) | ||
| } else if (dispose) { | ||
| effect.dispose = () => { | ||
| effect.disposed = true | ||
| return dispose() | ||
| } | ||
| runningEffects.push(effect) | ||
| } | ||
| } | ||
| return runningEffects | ||
| } | ||
| export function cleanEffects(runningEffects = []) { | ||
| for (const effect of runningEffects) { | ||
| effect.dispose() | ||
| } | ||
| return [] | ||
| } |
+67
| import { useReducer, useEffect, useCallback, useMemo, useRef } from 'react' | ||
| import { createMachine, transition, runEffects, cleanEffects } from './core.js' | ||
| function initial({ context, machine }) { | ||
| const initialState = { context } | ||
| const initialEvent = { type: null } | ||
| const [state, effects] = transition(machine, initialState, initialEvent) | ||
| const curr = { | ||
| machine, | ||
| effects: effects || [], | ||
| state, | ||
| } | ||
| return curr | ||
| } | ||
| function reduce(curr, action) { | ||
| if (action.type === 'send') { | ||
| const { event, machine } = action | ||
| const [state, effects] = transition(machine, curr.state, event) | ||
| return { ...curr, state, effects: effects || curr.effects } | ||
| } | ||
| } | ||
| export function useMachine(create, context = {}, options = {}) { | ||
| const { assign = 'assign', deps } = options | ||
| const runningEffects = useRef() | ||
| const firstRender = useRef(true) | ||
| const machine = useMemo(() => createMachine(create), [create]) | ||
| const [curr, dispatch] = useReducer(reduce, { context, machine }, initial) | ||
| const send = useCallback((event) => dispatch({ type: 'send', event, machine }), [ | ||
| machine, | ||
| dispatch, | ||
| ]) | ||
| useEffect(() => { | ||
| runningEffects.current = cleanEffects(runningEffects.current) | ||
| if (curr.effects.length) { | ||
| runningEffects.current = runEffects(curr.effects, curr.state, send) | ||
| } | ||
| }, [send, curr.effects]) | ||
| useEffect(() => { | ||
| return () => { | ||
| runningEffects.current = cleanEffects(runningEffects.current) | ||
| } | ||
| }, []) | ||
| const assignEffectDeps = [send].concat(deps || (context ? Object.values(context) : [])) | ||
| useEffect(() => { | ||
| if (firstRender.current) { | ||
| firstRender.current = false | ||
| return | ||
| } | ||
| if (assign) { | ||
| send({ type: assign, ...context }) | ||
| } | ||
| }, assignEffectDeps) | ||
| return [curr.state, send, curr.machine] | ||
| } |
+2
| export { createMachine, transition, runEffects, cleanEffects } from './core.js' | ||
| export { useMachine } from './hooks.js' |
+20
| The MIT License (MIT) | ||
| Copyright (c) 2020 Karolis Narkevicius | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| this software and associated documentation files (the "Software"), to deal in | ||
| the Software without restriction, including without limitation the rights to | ||
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
| the Software, and to permit persons to whom the Software is furnished to do so, | ||
| subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
| FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
| COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
| IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
Sorry, the diff of this file is too big to display
+61
| import { createMachine, transition, runEffects as run, cleanEffects as clean } from './core.js' | ||
| export function createService(machineDescription, context = {}) { | ||
| const machine = createMachine(machineDescription) | ||
| // initial transition | ||
| const [state, effects] = transition(machine, { name: null, context }, { type: null }) | ||
| let cbs = [] | ||
| let running = true | ||
| const service = { | ||
| machine, | ||
| state, | ||
| prev: null, | ||
| pendingEffects: effects || [], | ||
| runningEffects: [], | ||
| send, | ||
| subscribe, | ||
| stop, | ||
| } | ||
| function runEffects() { | ||
| service.runningEffects = clean(service.runningEffects) | ||
| service.runningEffects = run(service.pendingEffects, service.state, service.send) | ||
| } | ||
| function stop() { | ||
| running = false | ||
| cbs = [] | ||
| service.pendingEffects = [] | ||
| service.runningEffects = clean(service.runningEffects) | ||
| } | ||
| function subscribe(fn) { | ||
| cbs.push(fn) | ||
| return () => { | ||
| cbs = cbs.filter((f) => f !== fn) | ||
| } | ||
| } | ||
| function send(event) { | ||
| if (!running) return | ||
| service.prev = service.state | ||
| const [state, effects] = transition(service.machine, service.state, event) | ||
| service.state = state | ||
| if (effects) { | ||
| service.pendingEffects = effects | ||
| runEffects() | ||
| } | ||
| for (const cb of cbs) { | ||
| cb(state) | ||
| } | ||
| } | ||
| runEffects() | ||
| return service | ||
| } |
+165
| export type EventType = string; | ||
| export type MetaObject = Record<string, any>; | ||
| export interface EventObject { | ||
| type: string; | ||
| } | ||
| export type Event<TEvent extends EventObject> = TEvent['type'] | TEvent; | ||
| export interface StateObject<TContext = any> { | ||
| name: string | ||
| context: TContext | ||
| final?: true | ||
| } | ||
| export interface StateSchema<TContext = any> { | ||
| meta?: any; | ||
| context?: Partial<TContext>; | ||
| states?: { | ||
| [key: string]: StateSchema<TContext>; | ||
| }; | ||
| } | ||
| export function useMachine<TContext, TStateSchema extends StateSchema, TEvent extends EventObject>( | ||
| description: MachineDescription<TContext, TStateSchema, TEvent>, | ||
| context?: TContext, | ||
| options?: MachineOptions | ||
| ): [state: StateObject<TContext>, send: SendFunction<TEvent>] | ||
| export interface MachineOptions { | ||
| assign: string | boolean, | ||
| deps: string[] | ||
| } | ||
| export type MachineDescription<C, S, E> = ({ state, transition, immediate, internal, enter, exit }: { | ||
| state: StateFunction | ||
| transition: TransitionFunction<C, E> | ||
| immediate: ImmediateFunction<C, E> | ||
| internal: TransitionFunction<C, E> | ||
| enter: EnterFunction<C, E> | ||
| exit: ExitFunction<C, E> | ||
| }) => any | ||
| export type StateFunction = ( | ||
| name: string, | ||
| ...opts: (Transition | Immediate | Internal | Enter | Exit)[] | ||
| ) => MachineState | ||
| /** | ||
| * A `transition` function is used to move from one state to another. | ||
| * | ||
| * @param event - This will give the name of the event that triggers this transition. | ||
| * @param target - The name of the destination state. | ||
| * @param opts - Transition hooks, one of reduce, assign, guard or action. | ||
| */ | ||
| export type TransitionFunction<C, E> = ( | ||
| event: string, | ||
| target: string, | ||
| opts: TransitionOptions<C, E> | ||
| ) => Transition | ||
| /** | ||
| * An `immediate` transition is triggered immediately upon entering the state. | ||
| * | ||
| * @param target - The name of the destination state. | ||
| * @param opts - Transition hooks, one of reduce, assign, guard or action. | ||
| */ | ||
| export type ImmediateFunction<C, E> = ( | ||
| target: string, | ||
| opts: TransitionOptions<C, E> | ||
| ) => Immediate | ||
| /** | ||
| * An `internal` transition will re-enter the same state, but without re-runing enter/exit hooks. | ||
| * | ||
| * @param event - This will give the name of the event that triggers this transition. | ||
| * @param opts - Transition hooks, one of reduce, assign, guard or action. | ||
| */ | ||
| export type InternalFunction<C, E> = ( | ||
| target: string, | ||
| opts: TransitionOptions<C, E> | ||
| ) => Internal | ||
| export type EnterFunction<C, E> = ( | ||
| opts: EnterOptions<C, E> | ||
| ) => Enter | ||
| export type ExitFunction<C, E> = ( | ||
| opts: ExitOptions<C, E> | ||
| ) => Exit | ||
| export interface MachineState { | ||
| name: string | ||
| transitions: Map<string, Transition[]> | ||
| immediates?: Map<string, Immediate[]> | ||
| enter: any[] | ||
| exit: any[] | ||
| final?: true | ||
| } | ||
| export interface Transition { | ||
| type: 'transition' | ||
| event: string | ||
| target: string | ||
| guards: any[] | ||
| reducers: any[] | ||
| } | ||
| export interface Immediate { | ||
| type: 'transition' | ||
| target: string | ||
| guards: any[] | ||
| reducers: any[] | ||
| } | ||
| export interface Internal { | ||
| type: 'transition' | ||
| internal: true | ||
| event: string | ||
| guards: any[] | ||
| reducers: any[] | ||
| } | ||
| export interface Enter { | ||
| type: 'enter' | ||
| reducers: any[] | ||
| effects: any[] | ||
| } | ||
| export interface Exit { | ||
| type: 'enter' | ||
| reducers: any[] | ||
| effects: any[] | ||
| } | ||
| export interface TransitionOptions<C, E> { | ||
| guard?: GuardFunction<C, E> | GuardFunction<C, E>[] | ||
| reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[] | ||
| assign?: Assign<C, E> | Assign<C, E>[] | ||
| action?: ActionFunction<C, E> | ActionFunction<C, E>[] | ||
| } | ||
| export interface EnterOptions<C, E> { | ||
| effect?: EffectFunction<C, E> | EffectFunction<C, E>[] | ||
| invoke?: InvokeFunction<C, E> | InvokeFunction<C, E>[] | ||
| reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[] | ||
| assign?: Assign<C, E> | Assign<C, E>[] | ||
| action?: ActionFunction<C, E> | ActionFunction<C, E>[] | ||
| } | ||
| export interface ExitOptions<C, E> { | ||
| reduce?: ReduceFunction<C, E> | ReduceFunction<C, E>[] | ||
| assign?: Assign<C, E> | Assign<C, E>[] | ||
| action?: ActionFunction<C, E> | ActionFunction<C, E>[] | ||
| } | ||
| export type ReduceFunction<C, E> = (context: C, event: E) => C | ||
| export type ActionFunction<C, E> = (context: C, event: E) => unknown | ||
| export type GuardFunction<C, E> = (context: C, event: E) => boolean | ||
| export type Assign<C, E> = true | Partial<C> | ((context: C, event: E) => Partial<C>) | ||
| export type InvokeFunction<C, E> = (context: C, event: E) => Promise<any> | ||
| export type EffectFunction<C, E> = (context: C, event: E) => CleanupFunction | void | ||
| export type CleanupFunction = () => void | ||
| export type SendFunction<TEvent extends EventObject> = (event: TEvent) => void |
| import { MachineDescription, useMachine } from 'react-machine' | ||
| interface LightStateSchema { | ||
| states: { | ||
| green: {}; | ||
| yellow: {}; | ||
| red: { | ||
| states: { | ||
| walk: {}; | ||
| wait: {}; | ||
| stop: {}; | ||
| }; | ||
| }; | ||
| }; | ||
| } | ||
| interface LightContext { | ||
| elapsed: number; | ||
| a: number | ||
| } | ||
| type LightEvent = | ||
| | { type: 'TIMER' } | ||
| | { type: 'POWER_OUTAGE' } | ||
| | { type: 'PED_COUNTDOWN', duration: number } | ||
| const machine: MachineDescription<LightContext, LightStateSchema, LightEvent> = ({ state, transition, enter }) => { | ||
| state('green', | ||
| transition('TIMER', 'yellow', { | ||
| reduce: (ctx, { type, ...data }) => { | ||
| if (type === 'PED_COUNTDOWN') { | ||
| return { ...ctx, a: data.duration } | ||
| } | ||
| return ctx | ||
| }, | ||
| guard: (ctx) => true | ||
| }), | ||
| enter({ effect: (ctx, event) => {}}) | ||
| ) | ||
| } | ||
| export function TypoComponent () { | ||
| const [state, send] = useMachine(machine) | ||
| } |
| { | ||
| "compilerOptions": { | ||
| "module": "commonjs", | ||
| "lib": ["es6"], | ||
| "target": "es6", | ||
| "noImplicitAny": true, | ||
| "noImplicitThis": true, | ||
| "strictNullChecks": true, | ||
| "strictFunctionTypes": true, | ||
| "noEmit": true, | ||
| "baseUrl": ".", | ||
| "paths": { "react-machine": ["."] } | ||
| } | ||
| } |
+69
-11
| { | ||
| "name": "react-machine", | ||
| "version": "0.1.0", | ||
| "description": "", | ||
| "main": "index.js", | ||
| "version": "0.2.0", | ||
| "description": "A lightweight state machine for React applications", | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./index.js", | ||
| "./core": "./core.js", | ||
| "./service": "./service.js", | ||
| "./hooks": "./hooks.js" | ||
| }, | ||
| "types": "types", | ||
| "sideEffects": false, | ||
| "files": [ | ||
| "*" | ||
| ], | ||
| "scripts": { | ||
| "test": "echo \"Error: no test specified\" && exit 1" | ||
| "test": "healthier && prettier --check '**/*.{js,css,yml}' && ava && npm run package-check", | ||
| "format": "prettier --write '**/*.{js,css,yml}'", | ||
| "package-check": "npm run build && cd dist && package-check", | ||
| "coverage": "c8 --reporter=html ava", | ||
| "build": "node ./build.js", | ||
| "watch": "nodemon --ignore dist ./build.js", | ||
| "version": "npm run build", | ||
| "release": "np --contents dist", | ||
| "release-beta": "np --tag=beta --contents=dist", | ||
| "types": "tsc" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Karolis Narkevicius <hello@kn8.lt>", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/tempname11/react-machine.git" | ||
| "url": "git+https://github.com/humaans/react-machine.git" | ||
| }, | ||
| "author": "", | ||
| "license": "MIT", | ||
| "bugs": { | ||
| "url": "https://github.com/tempname11/react-machine/issues" | ||
| "keywords": [ | ||
| "react", | ||
| "state", | ||
| "effects", | ||
| "react hook", | ||
| "state machine", | ||
| "finite state machine", | ||
| "finite automata" | ||
| ], | ||
| "dependencies": {}, | ||
| "peerDependencies": { | ||
| "react": "^17.0.1" | ||
| }, | ||
| "homepage": "https://github.com/tempname11/react-machine#readme" | ||
| } | ||
| "devDependencies": { | ||
| "@babel/cli": "^7.12.10", | ||
| "@babel/core": "^7.12.10", | ||
| "@babel/plugin-transform-modules-commonjs": "^7.12.1", | ||
| "@babel/plugin-transform-react-jsx": "^7.12.10", | ||
| "@babel/register": "^7.12.10", | ||
| "@skypack/package-check": "^0.2.2", | ||
| "ava": "^3.13.0", | ||
| "c8": "^7.3.5", | ||
| "esm": "^3.2.25", | ||
| "healthier": "^4.0.0", | ||
| "jsdom": "^16.4.0", | ||
| "np": "^7.0.0", | ||
| "prettier": "^2.2.1", | ||
| "react": "^17.0.1", | ||
| "react-dom": "^17.0.1", | ||
| "typescript": "^4.1.2" | ||
| }, | ||
| "ava": { | ||
| "files": [ | ||
| "test/test-*.js" | ||
| ], | ||
| "require": [ | ||
| "@babel/register" | ||
| ] | ||
| }, | ||
| "np": { | ||
| "releaseDraft": false | ||
| } | ||
| } |
Sorry, the diff of this file is not supported yet
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
354809
76203.01%12
300%8956
Infinity%1
-50%2
-33.33%324
32300%Yes
NaN1
Infinity%16
Infinity%2
Infinity%2
100%