# How Studio loads the project

How Studio loads courier.config and courier.mocks, reloads them on save, keeps running through failures, and how to run it from code.

## Loading

Before the config, Studio reads the project's variables as the application's development server does: from `.env.development.local`, `.env.local`, `.env.development` and `.env` in the project root — those there are — a variable keeping the first value it is given, and one Studio was started with keeping its own. `--dotenv` names other files instead, comma-separated, in the order they win. A source whose `baseURL` comes from `process.env` then has the address the application gives it — in the graph, in what a mock passes on to the API, in the documents Studio writes — and `courier-studio openapi`, `diff`, `verify`, `import` and `mcp` read them the same way. Studio watches the files, a file made after it started among them, and when one is saved it reads the variables again and loads the config with them: a variable the files no longer set is gone, one the process was started with keeps its own, and a save that changes no variable loads nothing. Its log names the variables that changed, never their values. The commands read the files once, as they start.

Studio loads `courier.config.*` from its project root — the working directory, or `--cwd` — and `courier.mocks.*` beside it, together. It watches both, every module of the project they import other than one imported for its types alone, which is erased before the config runs — the file that runs, so where a compiled `api.js` sits beside `api.ts` an import of `./api` runs and watches `api.js` — and the tsconfig files their path aliases are read from, the ones `tsconfig.json` extends included, and loads them again when one is saved; a config or mocks file created under another of its names is picked up too, and so is a module created where an import would run it instead, such as an `api.js` compiled beside the `api.ts` that ran. Every load — the first, each reload, and each edit Studio makes to `courier.mocks.ts` — runs on one chain, one at a time and in order, and a save made while a load runs is loaded once it ends. A Studio page that opens, reconnects or reloads is sent what Studio already runs, so it loads nothing.

Studio learns of a save from the system's file notifications, at once. It also compares what it watches with the disk: every half second on a disk whose saves arrive without a notification, so such a save is noticed within half a second too, and every 5 s elsewhere, as a safety net. A disk is taken as one without notifications when Linux reports it as an NFS, SMB, 9P, FUSE or VirtualBox shared-folder mount — a network share, a Windows drive under WSL, a folder shared into a virtual machine; when Studio finds two saves on it that no notification announced; and, every disk, when `COURIER_WATCH_POLLING` is set. Each comparison looks up every watched file that exists, and once each directory where a module may yet be created, rather than every path a module may take there.

An import follows the project's tsconfig `paths` as TypeScript does, except that an alias never takes an import of an installed package elsewhere: TypeScript tries the alias first, while the loader cannot fall back to the package when the alias leads nowhere. An alias for one package, such as `@acme/ui` to `../ui/src`, is not followed while that package is installed; one for a whole scope, `@acme/*`, is followed for the names in it the project did not install, so an installed `@acme/ui` loads the build it ships while `@acme/utils` loads from the alias; and a catch-all `*` is not followed.

A config loads only when every entry under `imports` has the fields `CourierImportEntry` declares, each of its kind — a `from` that is an http or https URL, a `validation` Studio knows, a `transform` that is a function; otherwise the load fails with a message naming the entry and the field.

While `courier.config` does not load, or after it is deleted, Studio keeps running the sources it loaded last, and says so:

- The APIs page names the file — with the loader's message when it does not load — and the time the sources it shows were loaded. A config that has never loaded gets its message instead of sources, and a project without one is told to create `courier.config.ts` and offered the config to start it from.
- The status bar shows `config error` or `config deleted` on every page, with the details as its title.
- **Import API** and **Generate mocks** are turned off, and say why.
- A request that writes an import's files, or cleans up after one, is refused with `{ error, code }`: 409 `CONFIG_NOT_LOADING` or `CONFIG_MISSING` until the config is fixed or created, and 503 `STUDIO_STARTING` or `CONFIG_RELOADING` while a load is still under way. Removing an import does not need the config to load.

One load may take 20 s. A load that takes longer — a config that awaits a database connection at top level, say — fails with `CONFIG_LOAD_TIMEOUT`, and Studio keeps running what it ran. The load itself goes on, since nothing can stop a module's evaluation. If it ends while its failure is still Studio's latest outcome, Studio takes how it ended: it runs the config when none of the files it read has changed since, loads it again when one has, and reports the real error when it failed. Until they end, loads given up keep their modules in memory; Studio warns at every fifth one still running, and a restart frees them. Studio reads its address — `studio.host` and `studio.port` — and `studio.mockBasePort` from its first load of the config, so the config is evaluated once as Studio starts. With both `--host` and `--port` given, Studio listens at once and serves the project as starting while that load runs; otherwise it listens once the load has an outcome. A first load that fails, or takes longer than the limit, leaves the flags and the defaults to decide. Studio reads these settings only when it starts: a change to them takes effect when it restarts.

Every load evaluates the config, the mocks file and the TypeScript modules they import again, so an edit to any of them is seen at once. A save that leaves such a file holding what Studio runs — a file saved unchanged, an edit undone before Studio reloads — loads nothing, and a file saved while a load was reading it is loaded once more, even when a second save put back what the load first read. What Studio reuses is each module's transform, while its bytes stay the same. It keeps those in `.cache/courier/transform` in the nearest `node_modules` at or above the project root, so the next start — and each mock's worker, and a build plugin reading Studio's address — transforms only what changed. Studio creates the directory with mode 0700 and deletes entries that have gone unused for 30 days. It keeps transforms in memory only, and logs why at debug level, when there is no `node_modules` (as with Yarn Plug'n'Play), when `COURIER_TRANSFORM_CACHE` turns the directory off, and on POSIX when `node_modules` or its `.cache` is a symbolic link or belongs to another user, or `courier` or `transform` in it is a link, belongs to another user or is writable by its group or others: the files there are code Studio runs. It never uses the system's temporary directory instead.

A module Node runs itself stays as Node first ran it until Studio restarts, so an edit to it is seen only then: a JavaScript module the config or the mocks file imports — `.mjs`, `.cjs` or `.js` — and any module other than JSON that a CommonJS config or mocks file loads with `require()`, TypeScript included.

| Variable | Unset | Set |
|---|---|---|
| `COURIER_CONFIG_LOAD_TIMEOUT_MS` | One load may take 20 s. | A whole number of milliseconds, from 1 to 86400000, gives a slow project longer. Read when Studio starts; any other value is ignored with a warning, in the terminal and the startup banner. |
| `COURIER_TRANSFORM_CACHE` | Transforms are kept in `node_modules/.cache/courier/transform`. | `0`, `false` or `off`, in any case, keeps them in memory only; `1`, `true` or `on` keeps the default. Studio warns once about any other value, and keeps the default. |
| `COURIER_WATCH_POLLING` | Studio compares the files it watches every half second only on a disk whose saves arrive without a notification, and every 5 s elsewhere. | `1`, `true` or `on`, in any case, compares them every half second on every disk — for one whose missing notifications Studio cannot tell; `0`, `false` or `off` keeps the default. Studio warns once about any other value, and keeps the default. |

## Programmatic

```ts
import { createStudioServer } from "@noverax/courier-studio/server"

const studio = createStudioServer({
  projectRoot: process.cwd(),
  onStarted: ({ url }) => console.log(`Studio at ${url}`),
  onError: (error) => console.error(error.message),
})

await studio.close()
```

### `createStudioServer(options?: StudioServerOptions): StudioServer`

Starts Studio and returns at once with `url`, `token` — the project's token, which a page opened at `` `${url}/#token=${token}` `` presents to make changes — and `close()`. Options: `port`, `host`, `projectRoot`, `mockBasePort`, `extraCorsOrigins` (more origins that may act as the Studio UI), `onReady`, `onStarted`, `onError`. `port`, `host` and `mockBasePort` left out are read from the `studio` block of the project's config, then default to 6280, `localhost` and 7001. Given both `port` and `host`, Studio listens at once; otherwise once its first load of the config has an outcome. `onReady` fires once it listens, with the URL a browser opens — a wildcard host reads as `localhost`, and with `port: 0` the URL carries the port the system picked. `onStarted` fires once it listens and that first load has an outcome, with `{ url, host, port, studio }`, where `studio` is the config's `studio` block — empty when there is none or the load failed. `onError` fires instead when it cannot listen — the port is taken, or out of range, or the host cannot be bound.
