Everything, in the order you reach for it
This page does not read front to back. It is the list you scan when you know what you want and cannot remember how it is spelled. Every entry links to the page that teaches it.
The five calls
All five come from @markless/core and are used inside a component body. Each block below is the
form, not a whole file; the linked page has a file to copy.
import { computed, element, shared, state, storage } from '@markless/core';
state(initial) declares a variable the page follows. Read it and assign it exactly like an
ordinary variable. See state.
let count = state(0);
count++;
computed(fn) declares a value worked out from other values, recalculated when one of its inputs
changes. Give it an async body and it becomes an async value, which needs a
@try boundary to be read from markup. See
computed.
const total = computed(() => shirts * 20 + mugs * 8);
const feed = computed(async () => fetchFeed(url.href));
storage(key, fallback) works like state, and also saves the value in the browser so it survives
a reload. Both arguments must be static strings. Omit the key and one is derived from the variable
name. See storage.
let theme = storage('theme', 'system');
element<T>() creates a handle you bind to one node with el, so later code can reach the real DOM
node. It reads undefined before the node exists and after it is gone, so read it with ?. inside
a handler. See elements.
let field = element<HTMLInputElement>();
// <input el={field} />
// field?.focus();
shared(fn) declares data that components resolve by calling it, instead of threading it down
through props. Read shared before you use it: on the published version
this site is built on, a definition in one module and a reader in another stalls the production
build, and passing the value through props is the working advice today.
The props every host element accepts
onClick={} and every other on<Event> name: runs when this node fires that DOM event
el={} binds one element() handle to this node
attach={} installs longer-lived behaviour on this node, and may return a cleanup function
class={} a string, including a state variable holding a string (see the caveat below)
style={} a style string
One caveat on class={}, and it is the site's own. On published 0.3.1 the compiler writes no DOM
update for a class binding, so a class that is an expression renders correctly from the server and
then never changes again. Styling has the measurement and what to
write instead.
attach belongs on a host element and never on a component. It may return a cleanup function, which
runs when the element goes away, and an array of behaviours installs in order and cleans up in
reverse.
The constructs
Control flow is part of the language, written with a leading @ inside markup. These too are forms
rather than files, written inside a component body.
@if (open) { <p>Shown</p> } @else { <p>Hidden</p> }
@switch (kind) {
@case 'alpha': { <p>A</p> }
@case 'beta': { <p>B</p> }
@default: { <p>Other</p> }
}
@for (const row of rows; index i; key row.id) {
<li>{i}. {row.label}</li>
} @empty {
<li>Nothing here yet</li>
}
@try {
<p>{feed.channel}</p>
} @pending {
<p>Checking...</p>
} @catch {
<p>That did not work</p>
}
The index and key clauses on @for are optional in syntax and not both optional in practice: an
interactive or stateful list needs a stable key, and key i is an explicit choice meaning state
follows the position rather than the item. @empty is the list's own branch for having no items.
See conditionals and lists.
@try / @pending / @catch is the only async status vocabulary. There is no status property to
read in a template, so everything you want to show about progress goes in a block. See
async.
The router
import { Html, Link } from '@markless/router';
import type { PageProps } from '@markless/router';
Html is the root element of document.tsrx, the single HTML shell around every page.
Link navigates on the client and renders a real <a>. Its href is the route pattern, spelled
the way the file is spelled, and params fills the holes. Optional props: prefetch, replace,
scroll. See links.
<Link href="/blog/[slug]" params={{ slug: post.slug }}>{post.title}</Link>
PageProps is what a page component receives: params for the bracketed segments, url with
href, pathname and search, and status. See page data.
File-to-URL mapping, and the two folders that are not pages:
pages/index.tsrx -> /
pages/about.tsrx -> /about
pages/blog/[slug].tsrx -> /blog/:slug
pages/docs/[...slug].mdx -> /docs/**
pages/404.tsrx or .mdx -> unmatched page requests, status 404
pages/500.tsrx or .mdx -> unhandled page rendering errors, status 500
api/ request handlers, top level, not under pages/
middleware/ code that runs before a request reaches a page
Rendering an app yourself
A router app never writes these. They are for a non-router app, and for tests.
import App from './App.tsrx';
import { render, renderToString } from '@markless/core';
render(App, { target: document.getElementById('app')! });
const html = renderToString(App);
You do not write a client entry, a server entry, a render shell, or a resume entry. The render specification names all four as things that are not valid app-authored requirements.
Creating a project
npm create markless@latest
The interactive flow asks what you are building, what to call it, where it should run, and whether
to install dependencies and start a repository, then shows a Ready to create? summary before
writing anything. See your first app.
Four starters:
Learn Markless minimal one page with a counter, the best first project
Build an app app document.tsrx plus 404 and 500 pages
Write docs docs an MDX docs site with a layout and sidebar components
Full-stack app full-stack app routes plus api/ and middleware/ files
Three runtimes, in this order: Node, Deno, Bun.
Flags for a run with no questions:
--yes accept the defaults for everything not given
--starter minimal | app | docs | full-stack
--format node | deno | bun
--no-install write the files, skip installing dependencies
--no-git write the files, skip the repository setup
The scripts in the generated package.json
vp is the vite-plus command line.
dev vp dev development server
build vp build production build
preview vp preview serve the production build
check vp check typecheck
fmt vp fmt format
test vp test run tests
doctor node scripts/markless-doctor.mjs environment, dependency and build sanity
Run doctor first for any environment, dependency or production-build failure. It checks that the
@markless/* dependencies exist and that their versions are aligned, because mismatched versions
cause protocol drift between the compiler and the runtime, and then it runs a production build.
The whole build wiring is two plugins:
import { markless } from '@markless/core/vite';
import { router } from '@markless/router/vite';
import { defineConfig } from 'vite-plus';
export default defineConfig({
plugins: [markless(), router()],
});
Diagnostics you can actually hit
Every one of these is a compile-time message with a code, a plain-language title, and a suggestion. Grouped by the part of the framework that raises them.
Importing and naming
MARKLESS_FRAMEWORK_IMPORT_REQUIRED. You called state, computed, element, shared or
storage without importing it, or you have a local function with that name. Import the API from
@markless/core, or rename your function.
MARKLESS_FRAMEWORK_API_ALIAS_UNSUPPORTED. You aliased one of those calls or passed it around as a
value. Call it directly, by its own name, at the place you want the graph node.
MARKLESS_SUBMODULE_UNSUPPORTED. The .tsrx file uses a submodule feature this host does not
support yet. Move the code into its own file.
Where state may be created
MARKLESS_STATE_MODULE_SCOPE. A state() or computed() sits at the top of a module instead of
inside a component body. Move it into the body of the component that owns it.
MARKLESS_ELEMENT_MODULE_SCOPE. Same thing for element(). A handle belongs to one component's
markup.
MARKLESS_STATE_CREATION_SITE_UNSTABLE. The call is somewhere the compiler cannot give a stable
identity, such as inside a branch or a loop body. Declare it once, in the body.
MARKLESS_STATE_CROSS_MODULE_IMPORT. You imported module-scope state from another module, which
cannot resume. Pass the value through props, or use storage() when it should be durable.
MARKLESS_STATE_DESTRUCTURE_DEFAULT_UNSUPPORTED. A destructuring default was used on graph state or
on a component's props. Declare the prop and pass the value explicitly.
Reading and writing
MARKLESS_STATE_WRITE_IN_TEMPLATE. An expression inside markup assigns to state. Markup describes
what is shown, so move the assignment into a handler.
MARKLESS_STATE_WRITE_IN_COMPUTED. A computed body assigns to state. A derived value may only
read. Return the new value instead.
MARKLESS_COMPUTED_DEPENDENCY_CYCLE. A computed depends on itself, directly or through a chain.
Break the loop by making one of the values plain state.
MARKLESS_TEMPLATE_AS_VALUE. Markup was used where a value was expected, for example assigned to a
variable. Markup is a statement, not an expression: put it where it should render.
MARKLESS_ATTRIBUTE_DUPLICATE. The same attribute is written twice on one element. Delete one.
Async
MARKLESS_ASYNC_BOUNDARY_REQUIRED. Markup reads an async computed with no @try around it. Wrap
the reads in @try, and give it @pending and @catch blocks.
MARKLESS_ASYNC_POST_AWAIT_READ. A reactive value is read after an await inside an async body,
which cannot be resumed. Read the values you need before the await and use the locals.
Lists
MARKLESS_REPEAT_KEY_REQUIRED. A @for whose rows carry state or handlers has no key clause. Add
key row.id, or whatever field identifies the item.
MARKLESS_REPEAT_KEY_IS_INDEX. The key is the loop index, so identity follows the position rather
than the item. Fine when rows never move, wrong when they do.
MARKLESS_REPEAT_KEY_UNSTABLE. The key expression is not stable across renders, for example a fresh
object or a random value. Key by a field that belongs to the item.
MARKLESS_REPEAT_COLLECTION_UNREADABLE. The compiler cannot see what the loop iterates. Give it a
variable holding an array rather than an expression it cannot follow.
Elements and behaviours
MARKLESS_ELEMENT_HANDLE_REQUIRED. el={} was given something that is not an element() handle.
MARKLESS_ELEMENT_HANDLE_UNBOUND. A handle is read but never bound to an element with el. Bind
it, or delete it.
MARKLESS_ELEMENT_HANDLE_DUPLICATE. One handle is bound to more than one element. A handle names
exactly one node, so make a second handle.
MARKLESS_ELEMENT_HANDLE_RENDER_READ. A handle is read while rendering, when there is no node yet.
Read it inside a handler.
MARKLESS_ATTACH_HOST_ELEMENT_REQUIRED. attach was put on a component. A component is not a node,
so there is nothing to hand the function. Move it to the host element inside.
MARKLESS_EVENT_SPREAD_UNSUPPORTED. An event handler arrived through a spread. Write the handler
prop on the element.
MARKLESS_SPREAD_STATIC_SNAPSHOT. Spread attributes render once and do not update afterwards. Bind
the attributes that change individually.
MARKLESS_STYLE_OBJECT_UNSUPPORTED. The style object shape is not one the compiler can turn into
attribute updates. Write style as a string, or move the rules into a scoped <style> block and
switch a class.
Components, storage and shared
MARKLESS_CALLBACK_PROP_ARITY_UNSUPPORTED. A callback prop takes more than one parameter. Pass one
object instead.
MARKLESS_STORAGE_KEY_STATIC. The key or the fallback given to storage() is not a literal string.
Both are baked in at compile time, so both must be literals.
MARKLESS_SHARED_SCOPE_INVALID. The scope given to shared() is not one it accepts.
MARKLESS_SHARED_DEFINITION_CYCLE. Two shared definitions depend on each other in a circle.
Router and payload
Route conflict. Two files map to the same URL. The message names both files, and the rule ignores
extensions, so docs.tsrx and docs.mdx conflict. Delete or rename one.
MARKLESS_ROUTER_DOCUMENT_STORAGE_UNSUPPORTED. document.tsrx declares storage() cells. The
router serves only the document's HTML, so their state payload never reaches the browser and they
can never resume. Declare storage() in a component the page renders instead.
MARKLESS_SERIALIZE_UNSUPPORTED_VALUE. A value in the graph cannot be written into the payload,
because it is a function, a live host object, or another runtime resource. The message names the
path to it. Move the resource into attach={...}, make the value serializable, or derive it with
computed().
Where to go next
For the idea behind all of this, read how it works. For the shortest path to something running, read your first app.
