Config evaluation
The config is TypeScript, evaluated by an engine embedded in the Rune binary. There is no Node
process, no tsc, and no toolchain to install. A cold evaluation is a few milliseconds and a warm
run reads one cached JSON file.
What works
TypeScript syntax is erased before evaluation. Annotations, interfaces and type aliases have no runtime effect, and constructs that do carry runtime meaning keep it.
Variables, template strings, spread, Object.fromEntries, functions, enum members and relative
imports all behave as expected.
Relative imports
A config may import other files in the repository with ./ and ../ specifiers. Each imported
file goes through the same pipeline, recursively.
Forward slashes and platform separators resolve to the same file, so a Windows and a Linux machine produce the same result and share one cache entry.
The rune object
What a config is allowed to know about the machine it is read on. It is an export of the package Rune supplies, not a global, so the published types describe it and no declaration file is needed:
The import works in any file in the graph, not only the entry config — which is where it is most useful, since shared fragments tend to live in a helper module:
Reads are recorded. A variable the config actually read enters the cache key; an unrelated variable changing does not throw away a valid entry.
Assignment throws. The object is frozen, and env is a proxy that rejects writes, so a config
cannot change what Rune resolves with by mutating it.
Reading rune without importing it is an error that names the import, so a config written against
an older version says what to add.
What does not work
The engine is not Node. Reaching for a Node global or module produces a message that lists what is available:
Type-only imports from npm packages are fine, because they are erased before anything runs:
The one package Rune supplies
@gio-labs/rune is answered from inside the binary rather than resolved from node_modules, so
the authoring style the types teach loads before a package manager has installed anything.
It exports defineConfig and nothing else, and defineConfig returns its argument unchanged — it
exists to carry the types. Importing any other name from it fails rather than handing the config an
undefined value. No other bare specifier is supplied.
Dynamic import() is rejected for cache integrity. A computed specifier cannot be found by the
static import walk, so it would escape the hashed import graph and serve a stale result.
Errors
A config that throws reports the file and a line in the TypeScript the user wrote, not in the JavaScript the engine ran. Type stripping reprints the file, so the two line numbers differ. Rune builds a source map on the failure path and remaps the frame, which costs nothing on the runs that succeed.
A syntax error names the file and points at the span. A missing relative import names both the missing file and the file that imported it.
Caching
The resolved config is stored at node_modules/.cache/rune/ under a key made from:
- the entry file's bytes
- the bytes of every transitively imported relative file
- the Rune binary version
- the platform
- the environment values the config read
The cache is content addressed. There is no expiry and no timestamp comparison, so restoring a file to its original bytes reuses the original entry.
Every cache failure degrades to full evaluation with nothing on stderr. An unreadable entry is a miss. An unwritable cache directory is a miss that still exits 0. A caching layer that can break a build is worse than no caching layer.
Clear it with:
Budget
The targets the implementation is held to: