JSOC
elements man jsoc Read as markdownJSOC is the configuration language. It is JSON with comments, unquoted keys,
trailing commas, and one function, env(), which reads an environment variable
while the file compiles. A .jsoc file is a module: import it like any other
file and the compiler evaluates it at build time, type checks every read of it,
and emits plain JSON. config.jsoc at the project root is one such file, the
one Elements itself reads; elements man config covers what goes in it. This
page is the language.
Syntax
{
// line comment
/* block comment */
name: "my_app", // unquoted key
"retry-count": 3, // quoted key when the name is not an identifier
tags: ["a", "b",], // trailing commas anywhere
debug: false,
nothing: null,
port: env<number>("PORT", 4000),
}
A value is a string, a number, true, false, null, an object, an array, or
an env() call. Strings take single or double quotes. Nothing else parses:
there are no expressions, no arithmetic, no string concatenation, no references
from one key to another, and no spreads. A file that needs to compute
a value computes it in TypeScript from the imported values instead.
A key that appears twice takes the last value, as JSON parsers do. The one
exception is the target rule below, where a server entry and a @browser entry
for the same key are two different values on purpose.
The env() Function
env() is the only call. It reads one named variable when the file is
evaluated and folds the value into the emitted JSON. Nothing reads the
environment at runtime.
env("KEY") // string | undefined: unset leaves it empty
env("KEY", "fallback") // string: the fallback when unset
env!("KEY") // string: a build error when unset
env<number>("PORT", 4000) // number: parsed, a build error when it does not parse
env<boolean>!("LIVE") // boolean, required
The type argument is string, number, or boolean, and nothing else. A
default must have that type. Without a type argument the default's type decides,
and with neither the value is a string.
Numbers parse as decimals. Booleans read false, no, n, 0, and f as
false, in any case, and everything else as true, including an empty value, so
LIVE= in an env file turns env<boolean>("LIVE", false) on. A value that
does not parse as the requested type is a build error at the call, with the
variable name and the text it held.
Where the values come from is elements man config: the process environment,
then config/env/<environment>.env on top of it.
Types
An import of a .jsoc file is typed from its contents. Literals widen to their
primitive: "dark" is string, 3 is number, true is boolean. Arrays
are arrays of their element type, objects are object types with exactly the
keys written, and env() calls have the types listed above. Reading a key the
file does not have is a compile error, and so is using a value as the wrong
type.
// app/shared/settings.jsoc
{
theme: "dark",
retries: env<number>("RETRIES", 3),
label: env("LABEL"),
}
import settings from "#app/shared/settings.jsoc";
let theme: string = settings.theme;
let retries: number = settings.retries;
let label: string = settings.label ?? "untitled"; // string | undefined without the ??
settings.colour; // error: no such key
config.jsoc is also checked against the shape Elements expects: an unknown
compiler option, a bad session.expires, or a deploy block with a missing field
is a build diagnostic on the line that has it.
Browser and Server
A .jsoc file is evaluated once for each target. Every key is server-only
unless it is tagged, so a secret written next to a public value stays on the
server without any arrangement on your part. A /** @browser */ tag on a
property makes that property and everything under it visible to the browser as
well.
{
/** @browser */
public: {
appName: "Acme",
features: { signups: true },
},
databaseUrl: env!("DATABASE_URL"),
}
The server sees all of it. The browser sees public and nothing else: a
container is present in the browser file when any key under it is, and an
untagged sibling is not. The same key may appear twice, once plain and once
tagged; the server takes the plain entry and the browser takes the tagged one,
whichever order they are written in.
The tag is what decides what ships. Reading an untagged key from
browser-reachable code is not a build error today: the browser module does not
contain the key, so the read is undefined in the browser. When the browser
needs a value, tag it.
Importing
import config from "#config"; // config.jsoc, through the import map
import settings from "#app/shared/settings.jsoc"; // any other .jsoc file
The .jsoc extension is written out; it is not searched for the way .ts is.
The same import works in a .ts file, in an .html template, on the server, and
in the browser, and each target loads its own projection. What the module holds
is the evaluated result. There is no parsing at runtime and no environment
read; config.database.host is a string that was decided when the build ran.
Hot Reload
A .jsoc file is a source like any other. Save it, or save an env file whose
variable it reads, and the project server evaluates it again, emits the module
again, rebuilds everything that imports it, and hot reloads the running program
and the open browsers. That is the difference from process.env: a value read
through env() in a .jsoc file follows an env file edit into the running
app, and a process.env read is fixed when the process starts. Keep
configuration in .jsoc; reach for process.env only for a value that has to
stay out of the build.
Related
config: whatconfig.jsocholds, the environments, and the env files.typescript: the compiler that checks.jsocalongside.tsand.html.build: the build loop that evaluates and emits.jsocmodules on save.