Manual JSOC

JSOC

elements man jsoc Read as markdown

JSOC 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

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. To compute a value, import the file and compute it in TypeScript. JSOC holds only literal values and env(), so every value is known when the file compiles and emits as plain JSON.

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() forms
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"), }
Typed reads
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 tag
{ /** @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.

To read a value in the browser, tag it. The browser module holds only tagged keys, so secrets stay on the server. Reading an untagged key in browser code is a build error at the read, and the error names the key. The import itself is fine everywhere, and server code, including the body of an @rpc function, reads every key. Reading a container such as config.stripe in the browser is fine when a key under it is tagged; its browser copy holds just those keys.

Importing

Imports
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 .ehtml 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: what config.jsoc holds, the environments, and the env files.
  • typescript: the compiler that checks .jsoc alongside .ts and .ehtml.
  • build: the build loop that evaluates and emits .jsoc modules on save.