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

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