Loading Rules and Plugins
Rules, rule sets, and plugins are all loaded through the same mechanism. This page is the normative reference for that mechanism: which files it will load, how it tells a package specifier from a local path, and what it deliberately does not check.
The loadable set
Section titled “The loadable set”TypeScript is the default authoring path. A .ts rule, rule set entry point, or plugin
loads with no build step and no Node flags — Thymian transpiles it on the fly.
The loadable set is closed:
.ts— loaded via jiti..js/.mjs/.cjs— loaded via nativeimport().
.mts and .cts are not supported. .d.ts is never loadable, whatever it contains —
a declaration file is not runnable source, regardless of its extension matching otherwise.
Bare vs local specifiers
Section titled “Bare vs local specifiers”Every specifier you pass to --rule-set, --plugin, a ruleSets: entry, or a plugin path:
is exactly one of two kinds, decided syntactically:
- Bare specifier — an installed package. It resolves through your project first, then
Thymian’s own install, using Node’s own resolver. The resolved file must be built JavaScript
(
.js/.mjs/.cjs). A package that ships unbuilt TypeScript is declined — it must publish built output. A Node.js builtin id (http,url, …) is also declined; builtins are not loadable user modules. - Local specifier — a relative (
./,../) or absolute path with an explicit extension. There is no extension guessing, noindex/directory resolution, and no<cwd>/<specifier>fallback — a bare specifier never falls back to a local file. If you meant a local file, write it as a path.
A ruleSets: config entry follows this same contract — a package (bare specifier) or a local
path (see the configuration schema). A glob is not
a valid entry; to select many local files at once, bundle them into a rule set that globs them
via its pattern: field.
The explicit-extension rule applies to the top-level specifier only. Imports inside a
loaded module keep working exactly as they do in any Node/TypeScript project — a .ts rule
that does import './helper.js' for a file actually named helper.ts resolves normally
through jiti/NodeNext, because that resolution never goes through this loading contract.
Globs are not a third specifier kind. The value you pass to --rule-set or a ruleSets:
entry is always one of the two kinds above — it resolves to a single module. That module may
then select its own member rule files with a glob, but the glob lives in the rule set’s
pattern: field, not in the specifier you pass. See Globs.
Extension matching is case-sensitive against the on-disk casing. A mis-cased extension
(e.g. requesting .TS for a file that is actually .ts) is declined, naming the casing
mismatch rather than silently loading or silently skipping it.
What is not checked at load
Section titled “What is not checked at load”Loading does not type-check. jiti strips TypeScript types to transpile a .ts file; it
does not check them. If you want type errors caught, run tsc --noEmit — typically in CI, since
it is a separate step from loading.
erasableSyntaxOnly is not required of your code. That restriction belongs to Node’s own
experimental type-stripping feature, which Thymian does not use. Because loading goes through
jiti instead, enum, namespace, parameter properties, and decorators all load normally.
tsconfig.json paths aliases are not honoured. This is deliberate, so module resolution
stays predictable regardless of which tsconfig.json (if any) happens to be nearby. Do not
configure paths and expect Thymian to resolve through them.
Rule sets are flat
Section titled “Rule sets are flat”A rule set’s glob pattern selects individual rule files. A rule set cannot contain another rule set — if a glob match’s default export is itself a rule set, that is an error, not a nested load.
A rule set selects its member rule files with a glob — the pattern: field on the rule set
module it exports (not the --rule-set/ruleSets: value itself, which resolves to that
module as a bare or local specifier). Glob matching has a few
fixed behaviours:
node_modulesis excluded by default.- A rule set’s own file is excluded from its own matches (it can’t self-select).
- A match that resolves to a non-loadable kind (wrong extension,
.d.ts, a broken symlink, …) is skipped, with a stated reason — it does not fail the set by itself. - A matched file that fails while loading (a syntax error, a throwing top-level statement) fails the whole set.
- A glob that matched files but produced zero loadable rules throws — it never silently resolves to an empty rule set.
- Load order is deterministic: matches are sorted before loading.
Where transpiled output is cached
Section titled “Where transpiled output is cached”jiti’s filesystem transpile cache is pinned to ~/.cache/thymian/jiti — per-user, and
deliberately not the shared OS temp directory (a world-writable cache location would let
another local user plant a poisoned entry).
Migration
Section titled “Migration”If you’re updating from before this loading contract, these are the breaking changes:
- Extensionless top-level specifiers no longer resolve. Write the extension explicitly:
--rule-set my-rule→--rule-set ./my-rule.ts. - The
<cwd>/<specifier>bare-to-local fallback is gone. A local file must be written as a path (./or../or absolute) — a bare-looking specifier no longer falls back to resolving against the working directory. .mts/.ctsare no longer loadable for rules, rule sets, or plugins. Use.ts.- TypeScript-source packages in
node_modulesno longer load. A dependency must ship built JavaScript; publishing.tssource and relying on Thymian to transpile it is no longer supported.