π xiome.io
- π xiome components are universal plugins for websites
- π‘οΈ let your users enjoy a passwordless login experience
- π engage your users with cool features, like a questions board
- π° monetize your community with subscriptions and paywalls
- βοΈ let xiome cloud do the heavy lifting running the servers
- β‘ jumpstart your app by using xiome's auth system
- create your community at https://xiome.io/setup
- copy-paste your community's html install snippet into your web page's html
<head>
- copy-paste any components you like from https://xiome.io/components into your website's
<body>
(click to show details)
Β fundamental skills
- βΉοΈ you don't have to master these skills. just be aware of them, so you know what to study when you encounter the need
- learn how to use git and github so you can collaborate
- fork projects on github
- use a visualization tool like
gitk
to understand git graphs - make and manage branches
- add and reset staged changes, make and amend commits
- manage git remotes, fetch, pull, and pull branches
- interactive rebase to rewrite and cleanup history
- keep your branches up to date by rebasing onto master regularly
- resolve and merge conflicts
- make pull requests on github, and respond to code reviews
- learn the basics of using a bash shell
- learn the basics of typescript
- learn npm to install dependencies and run npm package scripts
- learn how to write code that blends in with the style and formatting of the rest of the codebase
- please be aware of the whitespace you author (the vscode setting
Render whitespace
settingBoundary
is great for this)
Β technical prerequisites
- if you're on windows, first, setup wsl and learn how it works
- or otherwise install a linux virtual machine for development (we recommend debian+kde on vmware)
- install
git
,nodejs
,vscode
, and connect github with ssh keys
Β initial setup
- fork the xiome project on github, and git clone your fork
- open a terminal in your cloned directory, and run these commands
npm install
to install the project's dependenciesnpm run build
to run a full build of the project
Β during your development sessions
- run
npm install
if you've recently pulled any changes to the package json - run these background processes, each in their own terminal
npm run watch
to continually rebuild source files on savenpm start
to run a local http server at http://localhost:8080/
(if npm start fails, try it again, it often works the second time)- note: tmux is a great way to split terminal windows
- note: of course, when you're done, end each process by pressing
ctrl+c
- open vscode by running
code .
- open your web browser
- see the xiome website at http://localhost:8080/x/
- see the mocksite at http://localhost:8080/x/mocksite/
- disable your browser's caching
- open chrome's developer tools
- in the network tab, enable "disable cache" mode
- or find the equivalent in your plebeian browser
- now you are ready to code
- whenever you save a typescript or pug file, the watch routine will automatically rebuild it, then you can refresh the browser to see your changes
- you can press vscode hotkey
ctrl+shift+b
to run the typescript build, which allows vscode to nicely highlight typescript errors for you to address
Β xiome's mock mode and the mocksite
- xiome super-cool mock mode
- the watch routine builds xiome into "mock" mode
- in mock mode, no connections are made to any real apis, database, etc
- 100% of the code, even the backend code, is running locally within the browser
- this allows you to test xiome's features without the muss or fuss of externalities like running servers or databases
- this mock mode also provides a unified debugging experience within the browser (even for backend business logic)
- the "database" state is actually saved in
localStorage
- in the browser dev console, you can wipe clean all the state by calling
localStorage.clear()
then refreshing the page
- you can work on the xiome.io website itself at http://localhost:8080/x/
- you can develop new features in the "mocksite" at http://localhost:8080/x/mocksite/
- the mocksite is a crappy-looking site that mimics a website that had installed xiome
- it's a "proving grounds" for developing new features
- you can login with a special email address
[email protected]
, which is a special fake account on the mocksite that has administrator privileges
Β the more you know
- this is an open source project, all contributions are under the mit license
(click to show details)
everything about xiome is fully contained in this single git repository.
let's take a stroll through the codebase.
.github/
this is where the github actions live.
they do continuous integration and deployments..vscode/
this is where the settings for the vscode editor live.
it makes sure you'll indent with tabs, as jesus intended.helm/
this is where the scary kubernetes code lives.
it orchestrates the node servers in the cloud.s/
this is where all the typescript source code lives.
there is where the fun development happens.s/assembly/
this is where all the pieces of xiome are assembled, much like legos, into a working backend and frontend.
this place is honestly frightening.s/common/
this is where we should put xiome code that many features can share.s/features/
this is where the feature development happens.
each feature contains all its backend and frontend code.
you should feel cozy here.s/framework/
this is where xiome defines fundamental standards for the whole system.
important base classes, like component, which is based on lit-element.s/toolbox/
this is a big collection of assorted utilities.
code here may be useful outside of xiome.
some of these tools are candidates to become independent libraries one day.
some of these are experimental.s/types/
these are where some common system-wide types should go.
scripts/
just some handy shell scripts.
sometimes i usescripts/randomid
generate new ids.web/
this is where the https://xiome.io/ website lives.
and also the mocksite we use during development.x/
these are just build artifacts.
never write code here β it's deleted and regenerated every build.
(click to show details)
xiome's fun code is organized conceptually into features.
these features are probably what you want to work on.
so here's what's in a feature directory:
feature/components/
web components live here.
html/css/js that the user interacts with.
components are wired up to models.
models give the components the state they should render.
models also provide functions that components can use to do smart things.feature/models/
models are the brains behind components.
models coordinate state for components to render.
models act as a liaison between components and the backend.
models are objects that have properties and functions for the components to use.
often, models are locally caching information loaded from the backend.
there's only one instance of each model on the whole page.
models coordinate everything for components on a page-wide level.
generally, we want components to be dumb renderers, leaving the smart logic up to the models.
feature/api/
here's where you'll find the api services.
a service is part of the api. it exposes a bunch of functions.
each service has its own auth policy.
an auth policy is like a bouncer at a nightclub β it decides which users get access to the service's exposed functions.
here in the api directory, you'll find other backendy-things, like database table definitions and whatnot.
feature/coolfeature.test.ts
this is where we practice test driven development.
we're seriously trying to get better at this.feature/testing/
this is where we keep utilities and mocks and stuff for the tests to use.
if you think about xiome like an onion, you'll notice some distinct layers.
each layer has its own little landscape of concepts and tools you'll need to learn, if you want to get anything done.
- learn about modern web development
- learn about web components
- learn about the shadow dom
- learn about css custom properties, and css parts
- learn https://lit.dev/
- learn about xiome's
ops
- the components commonly use a system called
ops
for loading spinners - anything that needs a loading spinner (many things) uses
ops
- they're everywhere
- read how
ops
work further down the readme
- the components commonly use a system called
- learn about xio vs xiome components
- component with names starting with
xiome
are "wired up" with models and state management - components with names starting with
xio
are simpler standalone components without any wirings
- component with names starting with
- learn about xiome component wirings
- xiome components have a property called
this.share
, which is a bag of goodies, like models and other facilities - xiome components are wired up with state management, so components will re-render whenever the relevant model state is changed
- wirings happen in files with names like
integrate-blah-components.ts
- xiome components have a property called
- learn that the
theme.css.ts
exists- all components inherit common css in
s/framework/theme.css.ts
- all components inherit common css in
- learn about services
- we provide models with api service objects, which contain async api functions
- you just call the functions. those are api calls
- learn about
snapstate
- snapstate is the state management library for xiome
- a model should return
readable
state, but not thewritable
state - a model must return the relevant snapstate
subscribe
ortrack
function - learn more about
snapstate
further below in the readme
- learn about
ops
- they're for loading spinners
- they're everywhere because lots of things load
- read more aboutn
ops
further below in the readme
- learn
renraku
and how xiome uses it- xiome's api is built with https://github.com/chase-moskal/renraku
- every async function in
expose
takes a first argument calledauth
auth
is where you'll find info about the current userauth
is where you'll find the database tables you need to useauth
is returned by thepolicy
that each service has
- learn
dbmage
to interact with the database- never forget that every id must be a
dbmage.Id
instance in the database - dbmage is what powers the magical serverless mock mode for development
- learn more about
dbmage
further down
- never forget that every id must be a
- learn
darkvalley
for validation- it's just some functions for validating user inputs
- learn more about
darkvalley
further down
(click to show details)
- "ops" is xiome's system for displaying loading spinners for asynchronous operations
- it's designed to be compatbile with state management libraries β this is why ops are simple object literals, instead of fancy class instances with methods and stuff
-
import {ops, Op} from "./s/framework/ops.js"
- an
op
is an object that can be in one of four states:none
β the op is uninitializedloading
β the op is loadingerror
β an error has occurredready
β loading is done, the data is ready
ops
is a toolkit with functions to create or interpret op objects- create ops
ops.none()
β create an op innone
stateops.loading()
β create an op inloading
stateops.error("thing failed lol")
β create an op inerror
state, provide a reason stringops.ready(value)
β create an op inready
state, provide the data valueops.replaceValue(op, newValue)
β create an op with the same state as another op
- check the current state of an op (return a boolean)
ops.isNone(op)
ops.isLoading(op)
ops.isError(op)
ops.isReady(op)
- extract the value out of an op (or return undefined)
ops.value(op)
- typescript types
let textOp: Op<string> //<-- specify the typescript type of an op textOp = ops.ready("hello")
- select (return different values based on the state of an op)
const value = ops.select(op, { none: () => 1, loading: () => 2, error: reason => 3, ready: value => 4, })
- running async operations
(perform an async operation, while updating an op property)let textOp: Op const text = await ops.operation({ setOp: op => textOp = op, promise: fetchTextFromSomewhere(), errorReason: "failed to fetch the text", })
- consolidate many ops into one
(only in terms of state, value is discarded)const op = ops.combine(op1, op2, op3)
- debugging tools
ops.mode(op)
β return an op's mode expressed as a stringconsole.log(ops.debug(op))
β log the op's details for console debugging
- usage in components
- the xio-op component is for low-level control of op rendering
(you can customize the loading spinner and more)html`<xio-op .op=${op}></xio-op>`
- renders a loading spinner when the op is loading
- has a slot for each op state
- use renderOp to render a proper
<xio-id>
component for an opimport {renderOp} from "./s/framework/render-op.js" render() { return renderOp(op, value => html` <p>${value}</p> `) }
- render an op-wrapped value, but without any loading spinner
(no<xio-op>
component)import {whenOpReady} from "./s/framework/when-op-ready.js" render() { return whenOpReady(op, value => html` <p>${value}</p> `) }
- the xio-op component is for low-level control of op rendering
(click to show details)
- snapstate is an experimental little state management library. it's the minimalistic successor to
autowatcher
andhappystate
which were previous incarnations in its lineage. - the concept of snapstate, is that we create a
readable
andwritable
version of a state object. - this allow us to write functions that have write access to the state via
writable
, but then we only expose thereadable
to the outside - example
function makeCounterModel() { const state = snapstate({ count: 0, }) function increment() { state.writable.count += 1 } return { subscribe: state.subscribe, state: state.readable, increment, } } const counterModel = makeCounterModel() console.log(counterModel.state.count) // 0 counterModel.increment() console.log(counterModel.state.count) // 1 counterModel.subscribe(() => console.log(counterModel.state.count)) counterModel.increment() //> 2 counterModel.state.count = 4 // ERROR readonly
- we return the readable state, which provides read-only access to the state object.
- we also return action functions that have exclusive access to the writable state.
- we also return a subscribe function, to subscribe to state changes
- snapstate changes are debounced
const counterModel = makeCounterModel() counterModel.subscribe(() => console.log(counterModel.state.count)) counterModel.increment() counterModel.increment() counterModel.increment() //> 3
- this is very important to understand. this means changes don't instantly trigger the subscribed listeners
- instead, many changes to the state can be queued up in a single tick, then subscribe is called afterwards
- snapstate returns a
wait
functionasync function example() { const counterModel = makeCounterModel() let sideEffect = false counterModel.subscribe(() => { sideEffect = true }) counterModel.increment() console.log(sideEffect) // false await counterModel.wait() console.log(sideEffect) // true }
- this allows you to wait for the debounced subscribed effects to fire
- snapstate returns a
track
functionconst counterModel = makeCounterModel() counterModel.track(() => console.log(counterModel.state.count)) //> 0 counterModel.increment() //> 1
- track is similar to subscribe, but works like mobx autorun
- track will immediately execute the listener function, and internally record which state properties are read
- then, whenever those specific state properties are written to, it will fire the affected listener function
- this can be efficient, because your track won't be triggered for unrelated changes to other state properties
- track is similar to subscribe, but works like mobx autorun
- both
subscribe
andtrack
return stop functions to unsubscribeconst counterModel = makeCounterModel() const unsubscribe = counterModel.subscribe( () => console.log(counterModel.state.count) ) const untrack = counterModel.track( () => console.log(counterModel.state.count) ) //> 0 counterModel.increment() //> 1 //> 1 unsubscribe() untrack() counterModel.increment() //> (sweet silence)
(click to show details)
- π§ββοΈ dbmage is how xiome systems interact with the database.
- see dbmage's readme on github: https://github.com/chase-moskal/dbmage
(click to show details)
- darkvalley is xiome's validation system for user inputs
- it's used on the frontend and backend alike, for validating forms, and apis
- a darkvalley validator is a function that returns a "problems" array of strings
- the problem strings are meant to be user-readable
- darkvalley provides many functions that return validator functions
- let's make an example validator for a string
import {validator, string, minLength, maxLength, notWhitespace} from "./s/toolbox/darkvalley.js" const validateCoolString = validator<string>( // ^ // create a standard validator, // providing a typescript generic // for the type that it will accept string(), // <--------- require input to be a string minLength(1), // <----- require input length is at least 1 maxLength(10), // <---- require input length at most 10 notWhitespace(), // <-- require input isn't all whitespace ) const problems1 = validateCoolString("hello!") //= [] const problems2 = validateCoolString("") //= ["too small"] const problems3 = validateCoolString("abcdefghijk") //= ["too big"] const problems4 = validateCoolString(" ") //= ["can't be all whitespace"]
- if the resulting problems array is empty (problems.length === 0), then the input has passed validation
- darkvalley has functions to prepare many kinds of validators
- for example, let's validate an array of numbers between 0 and 100
import {validator, array, each, number, min, max} from "./s/toolbox/darkvalley.js" const validateNumberArray = validator<number[]>( array(), each( number(), min(0), max(100), ), ) const problems1 = validate([1, 2, 99]) //= [] const problems2 = validate([1, 2, "99"]) //= ["(3) must be a number"] const problems3 = validate([1, 2, -99]) //= ["(3) too small"] const problems4 = validate([101, 2, 99]) //= ["(1) too big"]
- okay, now let's validate a whole object, and all its contents
import {schema, validator, string, number, minLength, maxLength} from "./s/toolbox/darkvalley.js" const validateUserObject = schema<{ nickname: string karma: number }>( nickname: validator(string(), minLength(1), maxLength(10)), karma: validator(number()), ) const problems1 = validateUserProblems({nickname: "chase", karma: 99}) //= []
- darkvalley has a bunch of handy validator preppers
import {validator, string, regex, url, origin, email} from "./s/toolbox/darkvalley.js" const validateLetters = validator<string>( string(), regex(/[a-zA-Z]+/i, "must be letters"), ) const validateUrl = validator<string>(string(), url()) const validateOrigin = validator<string>(string(), origin()) const validateEmail = validator<string>(string(), email())
multi
allows multiple problems to be returned at once,
whereasvalidator
stops and returns the first problem encountered.import {validator, multi, string, minLength, notWhitespace} from "./s/toolbox/darkvalley.js" const validateName = validator<string>( string(), multi( // <-- multi allows multiple problems to be returned at once minLength(3), notWhitespace(), ), ) const problems1 = validateName(" ") //= ["too small", "can't be all whitespace"]
branch
is like an 'or' operator, ignoring problems in one branch if another passesimport {validator, branch, string, url, https, localhost} from "./s/toolbox/darkvalley.js" const validateHttpsOrLocalhost = validator<string>( string(), url(), branch( https(), localhost(), ), ) const problems1 = validateHttpsOrLocalhost("http://chasemoskal.com/") //= [ //= "must be secure, starting with 'https'", //= "or, must be a localhost address" //= ]
(click to show details)
we like to give little bitcoin rewards to show appreciation for good contributions.
how to participate:
- find a task on the issues page with a bounty
- post a comment and ask to be assigned
- do the work, make a good pull request
- post your public bitcoin deposit address into the issue
if we merge the work to master, you may be eligible to receive a reward.
but remember, there are no guarantees: bounties are fun rewards, not contracts. the rules became too complicated, so now all bounties and rewards are arbitrated by chase moskal based on subjective factors and personal honor code.