Loading
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.tsand offered the config to start it from. - The status bar shows
config errororconfig deletedon 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 }: 409CONFIG_NOT_LOADINGorCONFIG_MISSINGuntil the config is fixed or created, and 503STUDIO_STARTINGorCONFIG_RELOADINGwhile 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
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.