This guide covers the development patterns and workflow, or how to build and test the engine.
Code is written in Typescript, built with make
, and runs in Node 12+.
Run make build
to compile into out/
.
Tests are written using Mocha, with Chai assertions and Sinon mocks/stubs/spies.
Run make test
to run tests (Mocha by itself). Run make cover
to run tests under nyc
,
and collect coverage reports into out/coverage/lcov-report/index.html
.
The engine can be run from make
or docker
with the demo data.
Run make run
to run the game normally. Run make run-image
to run the latest docker image (may pull the image).
The engine has a few builtin debug commands, which can be run from within the game:
debug
will print the current world state tree to outputgraph [path]
will print the current world state tree to a graphviz file
Run graph out/debug-graph
in the game, then make graph
normally to render the world tree.
Run make debug
to run the engine and wait for a Chrome inspector to be attached.
- sort imports
- modules
../
./
- sort declarations
- types
- interfaces (types and interfaces may need to be mixed)
- classes
- functions (avoid defining classes and loose functions in the same file)
- constants
- visibility
- prefer protected
- most methods should be public for testing
- private is a smell
- syntax
- do not use unary negation:
if (!foo)
- it is hard to read/easy to miss
- prefer type guards and positive assertions, they read better:
if (doesExist(foo))
- do not use
else if
, avoidelse
else if
should be a map lookup or switch, depending on the number of branches and whether it is dynamic- prefer early exit, it works better in async flows
- do not compare with
true
x === false
is a positive assertion (value is false)x === true
is usually only necessary whenx
might not be a boolean
- do not use unary negation:
- iteration
- prefer assertions for loop predicates
items.map(doesExist)
has all of the same semantic meaning (and less syntactic overhead) asitems.map((it) => doesExist)
- write composable functions with this in mind (not everything needs to be a method)
- JS and TS can do a limited form of point-free programming with semantic assertions and typeguards
- see tacit programming for more details
- prefer assertions for loop predicates
- models
- models should be POJSOs (Array, Map, and Set are still allowed)
- significant logic should live in a repository service (like the
StateService
does for game state)
- build
- bundling matters, never ship raw
node_modules
(for both inode counts and output size, tree-shaking) - hot module reloading never works reliably, don't bother
- bundling matters, never ship raw
- collections
- prefer
Map
overRecord
when keys are dynamic or iteration is needed
- prefer
- comparison
- always use strict boolean comparison
- prevents coercive comparison
- prefer semantic assertions over values (limited point-free style)
- always use strict boolean comparison
- operators
- do not use
==
, prefer===
==
too often requires an accompanying typeguard or defensive code- use narrow types and compare them narrowly
- do not use
- null and undefined
- do not use
null
, preferundefined
- Javascript (and by extension Typescript) has an inconsistent understanding of undefined values, with
keywords for explicit non-existance (
null
) and implicit non-existence (undefined
), in part due to the lack ofSome
andNone
types in the language - while
null
can be seen as explicit non-existence, it is effectively impossible to avoidundefined
while using JS, and using both is the most problematic option - the Ajv schemas can fail on missing values or insert default values during validation, ensuring models are populated and reducing defensive code
- the
@apextoaster/js-utils
library exports a number of assertive typeguards to remove null/undefined values
- do not use
- coverage
- coverage is a way of identifying unreachable and unused code
- 100% code coverage is not a goal, it is a side effect of removing dead code
- all new code should be fully tested
- once tests have been written for all expected behaviors, any uncovered code can be removed
- it is not needed to satisfy the requirements
- assertions
- prefer semantic assertions over value comparison:
isNil(x)
overx === null || x === undefined
- the assertion function can:
- have a doc comment
- be a type guard for a user-defined type
- the assertion function can:
- prefer positive assertions:
isNil(x)
is better thannotDefined(x)
- positive assertions usually have a finite result set, and describe the values that are present
- negative assertions only describe the values that are not present, which are no longer interesting
- prefer semantic assertions over asserting or coalescing operators
- prefer
mustExist(x).y
overx!.y
- a typed error with message can describe what was missing and what was expected, unlike a
TypeError
- this is a combination of the early-return, no-null, and typed-error patterns
- prefer
- prefer semantic assertions over value comparison:
- tests
- new code should have full coverage
- new code must not reduce overall coverage
- modified code should have full coverage
- regression tests should be added for every
type/bug
ticket- the test name or doc comment should have the ticket # or link
- these should usually be written against the broken version, then run against both broken and fixed versions